Skip to content

Commit

Permalink
stakingv3: rank dapps in-between tiers (#1240)
Browse files Browse the repository at this point in the history
  • Loading branch information
ermalkaleci authored Jun 18, 2024
1 parent 746df1a commit 3e87609
Show file tree
Hide file tree
Showing 19 changed files with 818 additions and 295 deletions.
7 changes: 6 additions & 1 deletion pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

#![cfg_attr(not(feature = "std"), no_std)]

use astar_primitives::dapp_staking::{DAppId, EraNumber, PeriodNumber, TierId};
use astar_primitives::dapp_staking::{DAppId, EraNumber, PeriodNumber, RankedTier, TierId};
use astar_primitives::BlockNumber;
pub use sp_std::collections::btree_map::BTreeMap;

Expand All @@ -27,6 +27,7 @@ sp_api::decl_runtime_apis! {
/// dApp Staking Api.
///
/// Used to provide information otherwise not available via RPC.
#[api_version(2)]
pub trait DappStakingApi {

/// How many periods are there in one cycle.
Expand All @@ -42,6 +43,10 @@ sp_api::decl_runtime_apis! {
fn blocks_per_era() -> BlockNumber;

/// Get dApp tier assignment for the given dApp.
#[changed_in(2)]
fn get_dapp_tier_assignment() -> BTreeMap<DAppId, TierId>;

/// Get dApp ranked tier assignment for the given dApp.
fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier>;
}
}
9 changes: 6 additions & 3 deletions pallets/dapp-staking-v3/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ mod benchmarks {

#[block]
{
let (dapp_tiers, _) = Pallet::<T>::get_dapp_tier_assignment_and_rewards(
let (dapp_tiers, _count) = Pallet::<T>::get_dapp_tier_assignment_and_rewards(
reward_era,
reward_period,
reward_pool,
Expand Down Expand Up @@ -1041,14 +1041,17 @@ mod benchmarks {
&cleanup_marker.dapp_tiers_index,
DAppTierRewardsFor::<T> {
dapps: (0..T::MaxNumberOfContracts::get())
.map(|dapp_id| (dapp_id as DAppId, 0))
.collect::<BTreeMap<DAppId, TierId>>()
.map(|dapp_id| (dapp_id as DAppId, RankedTier::new_saturated(0, 0)))
.collect::<BTreeMap<DAppId, RankedTier>>()
.try_into()
.expect("Using `MaxNumberOfContracts` as length; QED."),
rewards: vec![1_000_000_000_000; T::NumberOfTiers::get() as usize]
.try_into()
.expect("Using `NumberOfTiers` as length; QED."),
period: 1,
rank_rewards: vec![0; T::NumberOfTiers::get() as usize]
.try_into()
.expect("Using `NumberOfTiers` as length; QED."),
},
);

Expand Down
124 changes: 83 additions & 41 deletions pallets/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ pub use sp_std::vec::Vec;
use astar_primitives::{
dapp_staking::{
AccountCheck, CycleConfiguration, DAppId, EraNumber, Observer as DAppStakingObserver,
PeriodNumber, SmartContractHandle, StakingRewardHandler, TierId, TierSlots as TierSlotFunc,
PeriodNumber, Rank, RankedTier, SmartContractHandle, StakingRewardHandler, TierId,
TierSlots as TierSlotFunc,
},
oracle::PriceProvider,
Balance, BlockNumber,
Expand All @@ -71,7 +72,9 @@ mod benchmarking;
mod types;
pub use types::*;

pub mod migration;
pub mod weights;

pub use weights::WeightInfo;

const LOG_TARGET: &str = "dapp-staking";
Expand All @@ -91,7 +94,7 @@ pub mod pallet {
use super::*;

/// The current storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(7);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -198,6 +201,10 @@ pub mod pallet {
#[pallet::constant]
type NumberOfTiers: Get<u32>;

/// Tier ranking enabled.
#[pallet::constant]
type RankingEnabled: Get<bool>;

/// Weight info for various calls & operations in the pallet.
type WeightInfo: WeightInfo;

Expand Down Expand Up @@ -289,6 +296,7 @@ pub mod pallet {
beneficiary: T::AccountId,
smart_contract: T::SmartContract,
tier_id: TierId,
rank: Rank,
era: EraNumber,
amount: Balance,
},
Expand Down Expand Up @@ -1403,14 +1411,16 @@ pub mod pallet {
Error::<T>::RewardExpired
);

let (amount, tier_id) =
let (amount, ranked_tier) =
dapp_tiers
.try_claim(dapp_info.id)
.map_err(|error| match error {
DAppTierError::NoDAppInTiers => Error::<T>::NoClaimableRewards,
_ => Error::<T>::InternalClaimDAppError,
})?;

let (tier_id, rank) = ranked_tier.deconstruct();

// Get reward destination, and deposit the reward.
let beneficiary = dapp_info.reward_beneficiary();
T::StakingRewardHandler::payout_reward(&beneficiary, amount)
Expand All @@ -1423,6 +1433,7 @@ pub mod pallet {
beneficiary: beneficiary.clone(),
smart_contract,
tier_id,
rank,
era,
amount,
});
Expand Down Expand Up @@ -1670,7 +1681,7 @@ pub mod pallet {
}

/// Returns the dApp tier assignment for the current era, based on the current stake amounts.
pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, TierId> {
pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier> {
let protocol_state = ActiveProtocolState::<T>::get();

let (dapp_tiers, _count) = Self::get_dapp_tier_assignment_and_rewards(
Expand All @@ -1691,7 +1702,11 @@ pub mod pallet {
///
/// 2. Sort the entries by the score, in descending order - the top score dApp comes first.
///
/// 3. Read in tier configuration. This contains information about how many slots per tier there are,
/// 3. Calculate rewards for each tier.
/// This is done by dividing the total reward pool into tier reward pools,
/// after which the tier reward pool is divided by the number of available slots in the tier.
///
/// 4. Read in tier configuration. This contains information about how many slots per tier there are,
/// as well as the threshold for each tier. Threshold is the minimum amount of stake required to be eligible for a tier.
/// Iterate over tier thresholds & capacities, starting from the top tier, and assign dApps to them.
///
Expand All @@ -1705,10 +1720,6 @@ pub mod pallet {
/// ```
/// (Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search.)
///
/// 4. Calculate rewards for each tier.
/// This is done by dividing the total reward pool into tier reward pools,
/// after which the tier reward pool is divided by the number of available slots in the tier.
///
/// The returned object contains information about each dApp that made it into a tier.
/// Alongside tier assignment info, number of read DB contract stake entries is returned.
pub(crate) fn get_dapp_tier_assignment_and_rewards(
Expand Down Expand Up @@ -1737,47 +1748,16 @@ pub mod pallet {
// Sort by amount staked, in reverse - top dApp will end in the first place, 0th index.
dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1));

// 3.
// Iterate over configured tier and potential dApps.
// Each dApp will be assigned to the best possible tier if it satisfies the required condition,
// and tier capacity hasn't been filled yet.
let mut dapp_tiers = BTreeMap::new();
let tier_config = TierConfig::<T>::get();

let mut global_idx = 0;
let mut tier_id = 0;
for (tier_capacity, tier_threshold) in tier_config
.slots_per_tier
.iter()
.zip(tier_config.tier_thresholds.iter())
{
let max_idx = global_idx
.saturating_add(*tier_capacity as usize)
.min(dapp_stakes.len());

// Iterate over dApps until one of two conditions has been met:
// 1. Tier has no more capacity
// 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() {
if tier_threshold.is_satisfied(*stake_amount) {
global_idx.saturating_inc();
dapp_tiers.insert(*dapp_id, tier_id);
} else {
break;
}
}

tier_id.saturating_inc();
}

// In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold,
// one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists.
//
// In the current implementation, the dApp with the lower dApp Id has the advantage.
// There is no guarantee this will persist in the future, so it's best for dApps to do their
// best to avoid getting themselves into such situations.

// 4. Calculate rewards.
// 3. Calculate rewards.
let tier_rewards = tier_config
.reward_portion
.iter()
Expand All @@ -1791,6 +1771,67 @@ pub mod pallet {
})
.collect::<Vec<_>>();

// 4.
// Iterate over configured tier and potential dApps.
// Each dApp will be assigned to the best possible tier if it satisfies the required condition,
// and tier capacity hasn't been filled yet.
let mut dapp_tiers = BTreeMap::new();
let mut tier_slots = BTreeMap::new();

let mut upper_bound = Balance::zero();
let mut rank_rewards = Vec::new();

for (tier_id, (tier_capacity, tier_threshold)) in tier_config
.slots_per_tier
.iter()
.zip(tier_config.tier_thresholds.iter())
.enumerate()
{
let lower_bound = tier_threshold.threshold();

// Iterate over dApps until one of two conditions has been met:
// 1. Tier has no more capacity
// 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
for (dapp_id, staked_amount) in dapp_stakes
.iter()
.skip(dapp_tiers.len())
.take_while(|(_, amount)| tier_threshold.is_satisfied(*amount))
.take(*tier_capacity as usize)
{
let rank = if T::RankingEnabled::get() {
RankedTier::find_rank(lower_bound, upper_bound, *staked_amount)
} else {
0
};
tier_slots.insert(*dapp_id, RankedTier::new_saturated(tier_id as u8, rank));
}

// sum of all ranks for this tier
let ranks_sum = tier_slots
.iter()
.fold(0u32, |accum, (_, x)| accum.saturating_add(x.rank().into()));

let reward_per_rank = if ranks_sum.is_zero() {
Balance::zero()
} else {
// calculate reward per rank
let tier_reward = tier_rewards.get(tier_id).copied().unwrap_or_default();
let empty_slots = tier_capacity.saturating_sub(tier_slots.len() as u16);
let remaining_reward = tier_reward.saturating_mul(empty_slots.into());
// make sure required reward doesn't exceed remaining reward
let reward_per_rank = tier_reward.saturating_div(RankedTier::MAX_RANK.into());
let expected_reward_for_ranks =
reward_per_rank.saturating_mul(ranks_sum.into());
let reward_for_ranks = expected_reward_for_ranks.min(remaining_reward);
// re-calculate reward per rank based on available reward
reward_for_ranks.saturating_div(ranks_sum.into())
};

rank_rewards.push(reward_per_rank);
dapp_tiers.append(&mut tier_slots);
upper_bound = lower_bound; // current threshold becomes upper bound for next tier
}

// 5.
// Prepare and return tier & rewards info.
// In case rewards creation fails, we just write the default value. This should never happen though.
Expand All @@ -1799,6 +1840,7 @@ pub mod pallet {
dapp_tiers,
tier_rewards,
period,
rank_rewards,
)
.unwrap_or_default(),
counter,
Expand Down
Loading

0 comments on commit 3e87609

Please sign in to comment.