From 894d10d5c5e35fd72c357b224333f04fee259330 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 23 Oct 2023 16:20:31 +0200 Subject: [PATCH] Tier assignemnt --- pallets/dapp-staking-v3/src/lib.rs | 91 +++++++++++++-- .../dapp-staking-v3/src/test/tests_types.rs | 2 +- pallets/dapp-staking-v3/src/types.rs | 106 +++++++++++++++++- 3 files changed, 181 insertions(+), 18 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 5973042aa7..53583f956d 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -353,12 +353,17 @@ pub mod pallet { /// Tier configuration to be used during the newly started period #[pallet::storage] pub type NextTierConfig = - StorageValue<_, TierConfiguration, ValueQuery>; + StorageValue<_, TiersConfiguration, ValueQuery>; /// Tier configuration user for current & preceding eras. #[pallet::storage] pub type TierConfig = - StorageValue<_, TierConfiguration, ValueQuery>; + StorageValue<_, TiersConfiguration, ValueQuery>; + + /// Information about which tier a dApp belonged to in a specific era. + #[pallet::storage] + pub type DAppTiers = + StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; #[pallet::hooks] impl Hooks> for Pallet { @@ -411,8 +416,6 @@ pub mod pallet { ) } PeriodType::BuildAndEarn => { - // TODO: trigger dApp tier reward calculation here. This will be implemented later. - let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) let dapp_reward_pool = Balance::from(1_000_000_000u128); // TODO: same as above let era_reward = EraReward { @@ -421,6 +424,14 @@ pub mod pallet { dapp_reward_pool, }; + // Distribute dapps into tiers, write it into storage + let dapp_tier_rewards = Self::get_dapp_tier_assignment( + current_era, + protocol_state.period_number(), + dapp_reward_pool, + ); + DAppTiers::::insert(¤t_era, dapp_tier_rewards); + // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { // Store info about period end @@ -1282,13 +1293,13 @@ pub mod pallet { } // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. - pub fn dapp_tier_assignment(era: EraNumber, period: PeriodNumber) { + // TODO2: documentation + pub fn get_dapp_tier_assignment( + era: EraNumber, + period: PeriodNumber, + dapp_reward_pool: Balance, + ) -> DAppTierRewardsFor { let tier_config = TierConfig::::get(); - // TODO: this really looks ugly, and too complicated. Botom line is, this value has to exist. If it doesn't we have to assume it's `Default`. - // Rewards will just end up being all zeroes. - let reward_info = EraRewards::::get(Self::era_reward_span_index(era)) - .map(|span| span.get(era).map(|x| *x).unwrap_or_default()) - .unwrap_or_default(); let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); @@ -1307,6 +1318,11 @@ pub mod pallet { }; // TODO: maybe also push the 'Label' here? + // TODO2: proposition for label handling: + // Split them into 'musts' and 'good-to-have' + // In case of 'must', reduce appropriate tier size, and insert them at the end + // For good to have, we can insert them immediately, and then see if we need to adjust them later. + // Anyhow, labels bring complexity. For starters, we should only deliver the one for 'bootstraping' purposes. dapp_stakes.push((dapp_info.id, stake_amount.total())); } @@ -1314,7 +1330,60 @@ 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)); - // TODO: continue here + // 3. + // Calculate slices representing each tier + let mut dapp_tiers = Vec::with_capacity(dapp_stakes.len()); + + 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); + + // 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) + for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() { + if tier_threshold.is_satisfied(*stake_amount) { + global_idx.saturating_accrue(1); + dapp_tiers.push(DAppTier { + dapp_id: *dapp_id, + tier_id: Some(tier_id), + }); + } else { + break; + } + } + + tier_id.saturating_accrue(1); + } + + // 4. + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids) + // TODO: Sorting requirement can change - if we put it into tree, then we don't need to sort (explicitly at least). + // Important requirement is to have efficient deletion, and fast lookup. Sorted vector with entry like (dAppId, Option) is probably the best. + // The drawback being the size of the struct DOES NOT decrease with each claim. + // But then again, this will be used for dApp reward claiming, so 'best case scenario' (or worst) is ~1000 claims per day which is still very minor. + dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); + + // 5. Calculate rewards. + let tier_rewards = tier_config + .reward_portion + .iter() + .map(|percent| *percent * dapp_reward_pool) + .collect::>(); + + // 6. + // Prepare and return tier & rewards info + // In case rewards creation fails, we just write the default value. This should never happen though. + DAppTierRewards::, T::NumberOfTiers>::new( + dapp_tiers, + tier_rewards, + ) + .unwrap_or_default() } } } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 48fb3df715..24c92bf563 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1559,7 +1559,7 @@ fn tier_slot_configuration_basic_tests() { assert!(params.is_valid(), "Example params must be valid!"); // Create a configuration with some values - let init_config = TierConfiguration:: { + let init_config = TiersConfiguration:: { number_of_slots: 100, slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), reward_portion: params.reward_portion.clone(), diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 912019ce2a..d84f91b94f 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -34,6 +34,18 @@ use crate::pallet::Config; // Convenience type for `AccountLedger` usage. pub type AccountLedgerFor = AccountLedger, ::MaxUnlockingChunks>; +// Convenience type for `DAppTierRewards` usage. +pub type DAppTierRewardsFor = + DAppTierRewards, ::NumberOfTiers>; + +// Helper struct for converting `u16` getter into `u32` +pub struct MaxNumberOfContractsU32(PhantomData); +impl Get for MaxNumberOfContractsU32 { + fn get() -> u32 { + T::MaxNumberOfContracts::get() as u32 + } +} + /// Era number type pub type EraNumber = u32; /// Period number type @@ -1355,6 +1367,16 @@ pub enum TierThreshold { // Otherwise we could allow e.g. tier 3 to go below tier 4, which doesn't make sense. } +impl TierThreshold { + /// Used to check if stake amount satisfies the threshold or not. + pub fn is_satisfied(&self, stake: Balance) -> bool { + match self { + Self::FixedTvlAmount { amount } => stake >= *amount, + Self::DynamicTvlAmount { amount } => stake >= *amount, + } + } +} + /// Top level description of tier slot parameters used to calculate tier configuration. #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(NT))] @@ -1393,10 +1415,12 @@ impl> Default for TierParameters { } } +// TODO: refactor these structs so we only have 1 bounded vector, where each entry contains all the necessary info to describe the tier + /// Configuration of dApp tiers. #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(NT))] -pub struct TierConfiguration> { +pub struct TiersConfiguration> { /// Total number of slots. #[codec(compact)] pub number_of_slots: u16, @@ -1412,7 +1436,7 @@ pub struct TierConfiguration> { pub tier_thresholds: BoundedVec, } -impl> Default for TierConfiguration { +impl> Default for TiersConfiguration { fn default() -> Self { Self { number_of_slots: 0, @@ -1423,7 +1447,7 @@ impl> Default for TierConfiguration { } } -impl> TierConfiguration { +impl> TiersConfiguration { /// Check if parameters are valid. pub fn is_valid(&self) -> bool { let number_of_tiers: usize = NT::get() as usize; @@ -1435,7 +1459,7 @@ impl> TierConfiguration { && self.slots_per_tier.iter().fold(0, |acc, x| acc + x) == self.number_of_slots } - /// TODO + /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration. pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { let new_number_of_slots = Self::calculate_number_of_slots(native_price); @@ -1445,13 +1469,13 @@ impl> TierConfiguration { .clone() .into_inner() .iter() - .map(|x| *x * new_number_of_slots as u128) + .map(|percent| *percent * new_number_of_slots as u128) .map(|x| x.unique_saturated_into()) .collect(); let new_slots_per_tier = BoundedVec::::try_from(new_slots_per_tier).unwrap_or_default(); - // TODO: document this, and definitely refactor it to be simpler. + // TODO: document this! let new_tier_thresholds = if new_number_of_slots > self.number_of_slots { let delta_threshold_decrease = FixedU64::from_rational( (new_number_of_slots - self.number_of_slots).into(), @@ -1508,3 +1532,73 @@ impl> TierConfiguration { result.unique_saturated_into() } } + +/// Used to represent into which tier does a particular dApp fall into. +/// +/// In case tier Id is `None`, it means that either reward was already claimed, or dApp is not eligible for rewards. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub struct DAppTier { + /// Unique dApp id in dApp staking protocol. + #[codec(compact)] + pub dapp_id: DAppId, + /// `Some(tier_id)` if dApp belongs to tier and has unclaimed rewards, `None` otherwise. + pub tier_id: Option, +} + +/// Information about all of the dApps that got into tiers, and tier rewards +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(MD, NT))] +pub struct DAppTierRewards, NT: Get> { + /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime) + pub dapps: BoundedVec, + /// Rewards for each tier. First entry refers to the first tier, and so on. + pub rewards: BoundedVec, +} + +impl, NT: Get> Default for DAppTierRewards { + fn default() -> Self { + Self { + dapps: BoundedVec::default(), + rewards: BoundedVec::default(), + } + } +} + +impl, NT: Get> DAppTierRewards { + /// Attempt to construct `DAppTierRewards` struct. + /// If the provided arguments exceed the allowed capacity, return an error. + pub fn new(dapps: Vec, rewards: Vec) -> Result { + let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; + let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; + Ok(Self { dapps, rewards }) + } + + /// Consume reward for the specified dapp id, returning its amount. + /// In case dapp isn't applicable for rewards, or they have already been consumed, returns **zero**. + pub fn consume(&mut self, dapp_id: DAppId) -> Balance { + // Check if dApp Id exists. + let dapp_idx = match self + .dapps + .binary_search_by(|entry| entry.dapp_id.cmp(&dapp_id)) + { + Ok(idx) => idx, + // dApp Id doesn't exist + _ => return Balance::zero(), + }; + + match self.dapps.get_mut(dapp_idx) { + Some(dapp_tier) => { + if let Some(tier_id) = dapp_tier.tier_id.take() { + self.rewards + .get(tier_id as usize) + .map_or(Balance::zero(), |x| *x) + } else { + // In case reward has already been claimed + Balance::zero() + } + } + // unreachable code, at this point it was proved that index exists + _ => Balance::zero(), + } + } +}