From 0a7c5191d9cda058d09eb69f024c6d5d75b46502 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 11 Oct 2023 19:29:33 +0200 Subject: [PATCH 01/86] EraRewardSpan --- pallets/dapp-staking-v3/src/lib.rs | 10 ++ pallets/dapp-staking-v3/src/types.rs | 137 ++++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index ba4e77f907..a150a17be4 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -926,6 +926,16 @@ pub mod pallet { Ok(()) } + + /// TODO + #[pallet::call_index(11)] + #[pallet::weight(Weight::zero())] + pub fn claim_staker_reward(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + Ok(()) + } } impl Pallet { diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 5f5bea6586..fc76de3419 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -695,17 +695,6 @@ where } } -/// Rewards pool for stakers & dApps -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct RewardInfo { - /// Rewards pool for accounts which have locked funds in dApp staking - #[codec(compact)] - pub participants: Balance, - /// Reward pool for dApps - #[codec(compact)] - pub dapps: Balance, -} - // TODO: it would be nice to implement add/subtract logic on this struct and use it everywhere // we need to keep track of staking amount for periods. Right now I have logic duplication which is not good. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] @@ -776,8 +765,6 @@ impl StakeAmount { /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { - /// Info about era rewards - pub rewards: RewardInfo, /// How much balance is considered to be locked in the current era. /// This value influences the reward distribution. #[codec(compact)] @@ -850,7 +837,6 @@ impl EraInfo { /// ## Args /// `next_period_type` - `None` if no period type change, `Some(type)` if `type` is starting from the next era. pub fn migrate_to_next_era(&mut self, next_period_type: Option) { - self.rewards = Default::default(); self.active_era_locked = self.total_locked; match next_period_type { // If next era marks start of new voting period period, it means we're entering a new period @@ -1262,3 +1248,126 @@ impl ContractStakingInfoSeries { } } } + +/// Information required for staker reward payout for a particular era. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct EraReward { + /// Total reward pool for staker rewards + #[codec(compact)] + staker_reward_pool: Balance, + /// Total amount which was staked at the end of an era + #[codec(compact)] + staked: Balance, +} + +impl EraReward { + /// Create new instance of `EraReward` with specified `staker_reward_pool` and `staked` amounts. + pub fn new(staker_reward_pool: Balance, staked: Balance) -> Self { + Self { + staker_reward_pool, + staked, + } + } + + /// Total reward pool for staker rewards. + pub fn staker_reward_pool(&self) -> Balance { + self.staker_reward_pool + } + + /// Total amount which was staked at the end of an era. + pub fn staked(&self) -> Balance { + self.staked + } +} + +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum EraRewardSpanError { + /// Provided era is invalid. Must be exactly one era after the last one in the span. + InvalidEra, + /// Span has no more capacity for additional entries. + NoCapacity, +} + +/// Used to efficiently store era span information. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct EraRewardSpan> { + /// Span of EraRewardInfo entries. + span: BoundedVec, + /// The first era in the span. + #[codec(compact)] + first_era: EraNumber, + /// The final era in the span. + #[codec(compact)] + last_era: EraNumber, +} + +impl EraRewardSpan +where + SL: Get, +{ + /// Create new instance of the `EraRewardSpan` + pub fn new() -> Self { + Self { + span: Default::default(), + first_era: 0, + last_era: 0, + } + } + + /// First era covered in the span. + pub fn first_era(&self) -> EraNumber { + self.first_era + } + + /// Last era covered in the span + pub fn last_era(&self) -> EraNumber { + self.last_era + } + + /// Span length. + pub fn len(&self) -> usize { + self.span.len() + } + + /// `true` if span is empty, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.first_era == 0 && self.last_era == 0 + } + + /// Push new `EraReward` entry into the span. + /// If span is non-empty, the provided `era` must be exactly one era after the last one in the span. + pub fn push( + &mut self, + era: EraNumber, + era_reward: EraReward, + ) -> Result<(), EraRewardSpanError> { + // First entry, no checks, just set eras to the provided value. + if self.span.is_empty() { + self.first_era = era; + self.last_era = era; + self.span + .try_push(era_reward) + .map_err(|_| EraRewardSpanError::NoCapacity) + } else { + // Defensive check to ensure next era rewards refers to era after the last one in the span. + if era != self.last_era.saturating_add(1) { + return Err(EraRewardSpanError::InvalidEra); + } + + self.last_era = era; + self.span + .try_push(era_reward) + .map_err(|_| EraRewardSpanError::NoCapacity) + } + } + + /// Get the `EraReward` entry for the specified `era`. + /// + /// In case `era` is not covered by the span, `None` is returned. + pub fn get(&self, era: EraNumber) -> Option<&EraReward> { + match self.first_era.checked_sub(era) { + Some(index) => self.span.get(index as usize), + None => None, + } + } +} From e598d1b32ec18c607834d32803a5df3dadc45509 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 12 Oct 2023 18:27:29 +0200 Subject: [PATCH 02/86] Initial version of claim_staker_reward --- pallets/dapp-staking-v3/src/lib.rs | 131 +++++++++++++++++- pallets/dapp-staking-v3/src/test/mock.rs | 1 + .../dapp-staking-v3/src/test/tests_types.rs | 61 ++++++++ pallets/dapp-staking-v3/src/types.rs | 110 ++++++++++++++- 4 files changed, 297 insertions(+), 6 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index a150a17be4..65012f9bcd 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -45,7 +45,10 @@ use frame_support::{ weights::Weight, }; use frame_system::pallet_prelude::*; -use sp_runtime::traits::{BadOrigin, Saturating, Zero}; +use sp_runtime::{ + traits::{BadOrigin, Saturating, Zero}, + Perbill, +}; use astar_primitives::Balance; @@ -104,6 +107,15 @@ pub mod pallet { #[pallet::constant] type StandardErasPerBuildAndEarnPeriod: Get; + /// Maximum length of a single era reward span length entry. + #[pallet::constant] + type EraRewardSpanLength: Get; + + /// Number of periods for which we keep rewards available for claiming. + /// After that period, they are no longer claimable. + #[pallet::constant] + type RewardRetentionInPeriods: Get; + /// Maximum number of contracts that can be integrated into dApp staking at once. #[pallet::constant] type MaxNumberOfContracts: Get; @@ -192,6 +204,12 @@ pub mod pallet { smart_contract: T::SmartContract, amount: Balance, }, + /// Account has claimed some stake rewards. + Reward { + account: T::AccountId, + era: EraNumber, + amount: Balance, + }, } #[pallet::error] @@ -245,6 +263,12 @@ pub mod pallet { NoStakingInfo, /// An unexpected error occured while trying to unstake. InternalUnstakeError, + /// Rewards are no longer claimable since they are too old. + StakerRewardsExpired, + /// There are no claimable rewards for the account. + NoClaimableRewards, + /// An unexpected error occured while trying to claim rewards. + InternalClaimStakerError, } /// General information about dApp staking protocol state. @@ -292,6 +316,24 @@ pub mod pallet { #[pallet::storage] pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; + /// Information about rewards for each era. + /// + /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. + /// For the sake of simplicity, valid `era` keys are calculated as: + /// + /// era_key = era - (era % T::EraRewardSpanLength) + /// + /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. + /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. + #[pallet::storage] + pub type EraRewards = + StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan, OptionQuery>; + + /// Information about period's end. + #[pallet::storage] + pub type PeriodEnd = + StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(now: BlockNumberFor) -> Weight { @@ -310,7 +352,10 @@ pub mod pallet { } let mut era_info = CurrentEraInfo::::get(); - let next_era = protocol_state.era.saturating_add(1); + let staker_reward_pool = Balance::from(1000_u128); // TODO: calculate this properly + let era_reward = EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + let ending_era = protocol_state.era; + let next_era = ending_era.saturating_add(1); let maybe_period_event = match protocol_state.period_type() { PeriodType::Voting => { // For the sake of consistency @@ -359,18 +404,28 @@ pub mod pallet { } }; + // Update storage items + protocol_state.era = next_era; ActiveProtocolState::::put(protocol_state); CurrentEraInfo::::put(era_info); + let era_span_index = Self::era_reward_span_index(ending_era); + let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); + // TODO: error must not happen here. Log an error if it does. + // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. + // Will require a fix from the runtime team though. + let _ = span.push(ending_era, era_reward); + EraRewards::::insert(&era_span_index, span); + Self::deposit_event(Event::::NewEra { era: next_era }); if let Some(period_event) = maybe_period_event { Self::deposit_event(period_event); } // TODO: benchmark later - T::DbWeight::get().reads_writes(2, 2) + T::DbWeight::get().reads_writes(3, 3) } } @@ -934,6 +989,71 @@ pub mod pallet { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; + let protocol_state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; + ensure!( + staked_period + >= protocol_state + .period_number() + .saturating_sub(T::RewardRetentionInPeriods::get()), + Error::::StakerRewardsExpired + ); + + // Calculate the reward claim span + let first_claim_era = ledger + .oldest_stake_era() + .ok_or(Error::::InternalClaimStakerError)?; + let era_rewards = EraRewards::::get(Self::era_reward_span_index(first_claim_era)) + .ok_or(Error::::InternalClaimStakerError)?; + + // TODO: need to know when period ended, storage item is needed for this. + // last_period_era or current_era - 1 + let last_period_era = if staked_period == protocol_state.period_number() { + protocol_state.era.saturating_sub(1) + } else { + PeriodEnd::::get(&staked_period) + .ok_or(Error::::InternalClaimStakerError)? + .final_era + }; + let last_claim_era = era_rewards.last_era().min(last_period_era); + + // Get chunks for reward claiming + let chunks_for_claim = ledger + .staked + .left_split(last_claim_era) + .map_err(|_| Error::::InternalClaimStakerError)?; + + // Calculate rewards + let mut rewards: Vec<_> = Vec::new(); + for era in first_claim_era..=last_claim_era { + let era_reward = era_rewards + .get(era) + .ok_or(Error::::InternalClaimStakerError)?; + + let chunk = chunks_for_claim + .get(era) + .ok_or(Error::::InternalClaimStakerError)?; + + let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) + * era_reward.staker_reward_pool(); + rewards.push((era, staker_reward)); + } + + let reward_sum = rewards.iter().fold(Balance::zero(), |acc, (_, reward)| { + acc.saturating_add(*reward) + }); + + T::Currency::deposit_into_existing(&account, reward_sum); + rewards.into_iter().for_each(|(era, reward)| { + Self::deposit_event(Event::::Reward { + account: account.clone(), + era, + amount: reward, + }); + }); + Ok(()) } } @@ -989,5 +1109,10 @@ pub mod pallet { IntegratedDApps::::get(smart_contract) .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) } + + /// Calculates the `EraRewardSpan` index for the specified era. + fn era_reward_span_index(era: EraNumber) -> EraNumber { + era.saturating_sub(era % T::EraRewardSpanLength::get()) + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index fb0cc65c27..398af7ab7d 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -112,6 +112,7 @@ impl pallet_dapp_staking::Config for Test { type StandardEraLength = ConstU64<10>; type StandardErasPerVotingPeriod = ConstU32<8>; type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; + type EraRewardSpanLength = ConstU32<8>; type MaxNumberOfContracts = ConstU16<10>; type MaxUnlockingChunks = ConstU32<5>; type MaxStakingChunks = ConstU32<8>; diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index ba80bfe295..bd5befa79b 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1774,3 +1774,64 @@ fn contract_staking_info_series_unstake_with_inconsistent_data_fails() { assert!(series.unstake(1, period_info, era - 2).is_err()); assert!(series.unstake(1, period_info, era - 1).is_ok()); } + +#[test] +fn era_reward_span_push_and_get_works() { + get_u32_type!(SpanLength, 8); + let mut era_reward_span = EraRewardSpan::::new(); + + // Sanity checks + assert!(era_reward_span.is_empty()); + assert!(era_reward_span.len().is_zero()); + assert!(era_reward_span.first_era().is_zero()); + assert!(era_reward_span.last_era().is_zero()); + + // Insert some values and verify state change + let era_1 = 5; + let era_reward_1 = EraReward::new(23, 41); + assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); + assert_eq!(era_reward_span.len(), 1); + assert_eq!(era_reward_span.first_era(), era_1); + assert_eq!(era_reward_span.last_era(), era_1); + + // Insert another value and verify state change + let era_2 = era_1 + 1; + let era_reward_2 = EraReward::new(37, 53); + assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); + assert_eq!(era_reward_span.len(), 2); + assert_eq!(era_reward_span.first_era(), era_1); + assert_eq!(era_reward_span.last_era(), era_2); + + // Get the values and verify they are as expected + assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); + assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); +} + +#[test] +fn era_reward_span_fails_when_expected() { + // Capacity is only 2 to make testing easier + get_u32_type!(SpanLength, 2); + let mut era_reward_span = EraRewardSpan::::new(); + + // Push first values to get started + let era_1 = 5; + let era_reward = EraReward::new(23, 41); + assert!(era_reward_span.push(era_1, era_reward).is_ok()); + + // Attempting to push incorrect era results in an error + for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { + assert_eq!( + era_reward_span.push(era_1 - 1, era_reward), + Err(EraRewardSpanError::InvalidEra) + ); + } + + // Pushing above capacity results in an error + let era_2 = era_1 + 1; + assert!(era_reward_span.push(era_2, era_reward).is_ok()); + let era_3 = era_2 + 1; + assert_eq!( + era_reward_span.push(era_3, era_reward), + Err(EraRewardSpanError::NoCapacity) + ); +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index fc76de3419..849dbf4846 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -47,6 +47,7 @@ pub type DAppId = u16; // TODO: perhaps this trait is not needed and instead of having 2 separate '___Chunk' types, we can have just one? /// Trait for types that can be used as a pair of amount & era. pub trait AmountEraPair: MaxEncodedLen + Default + Copy { + fn new(amount: Balance, era: EraNumber) -> Self; /// Balance amount used somehow during the accompanied era. fn get_amount(&self) -> Balance; /// Era acting as timestamp for the accompanied amount. @@ -72,6 +73,8 @@ pub enum AccountLedgerError { UnavailableStakeFunds, /// Unstake amount is to large in respect to what's staked. UnstakeAmountLargerThanStake, + /// Split era is invalid; it's not contained withing the vector's scope. + SplitEraInvalid, } /// Helper struct for easier manipulation of sparse pairs. @@ -226,8 +229,87 @@ where Ok(()) } + + // TODO: unit test + /// Splits the vector into two parts, using the provided `era` as the splitting point. + /// + /// All entries which satisfy the condition `entry.era <= era` are removed from the vector. + /// After split is done, the _removed_ part is padded with an entry, IFF the last element doesn't cover the specified era. + /// The same is true for the remaining part of the vector. + /// + /// The `era` argument **must** be contained within the vector's scope. + /// + /// E.g.: + /// a) [1,2,6,7] -- split(4) --> [1,2,4],[5,6,7] + /// b) [1,2] -- split(4) --> [1,2,4],[5] + /// c) [1,2,4,5] -- split(4) --> [1,2,4],[5] + /// d) [1,2,4,6] -- split(4) --> [1,2,4],[5,6] + pub fn left_split(&mut self, era: EraNumber) -> Result { + // TODO: this implementation can once again be optimized, sacrificing the code readability a bit. + // But I don't think it's worth doing, since we're aiming for the safe side here. + + // Split the inner vector into two parts + let (mut left, mut right): (Vec

, Vec

) = self + .0 + .clone() + .into_iter() + .partition(|chunk| chunk.get_era() <= era); + + if left.is_empty() { + return Err(AccountLedgerError::SplitEraInvalid); + } + + if let Some(&last_l_chunk) = left.last() { + // In case 'left' part is missing an entry covering the specified era, add it. + if last_l_chunk.get_era() < era { + let mut new_chunk = last_l_chunk; + new_chunk.set_era(era); + left.push(new_chunk); + } + + // In case 'right' part is missing an entry covering the specified era, add it. + match right.first() { + Some(first_r_chunk) if first_r_chunk.get_era() > era.saturating_add(1) => { + let mut new_chunk = last_l_chunk.clone(); + new_chunk.set_era(era.saturating_add(1)); + right.insert(0, new_chunk); + } + None => { + let mut new_chunk = last_l_chunk.clone(); + new_chunk.set_era(era.saturating_add(1)); + right.insert(0, new_chunk); + } + _ => (), + } + } + + // TODO: I could just use `expect` here since it's impossible for vectors to increase in size. + self.0 = BoundedVec::try_from(right).map_err(|_| AccountLedgerError::NoCapacity)?; + + Ok(Self( + BoundedVec::::try_from(left).map_err(|_| AccountLedgerError::NoCapacity)?, + )) + } + + // TODO: unit tests + /// Returns the most appropriate chunk for the specified era, if it exists. + pub fn get(&self, era: EraNumber) -> Option

{ + match self.0.binary_search_by(|chunk| chunk.get_era().cmp(&era)) { + // Entry exists, all good. + Ok(idx) => self.0.get(idx).map(|x| *x), + // Entry doesn't exist, but another one covers it so we return it. + Err(idx) if idx > 0 => self.0.get(idx.saturating_sub(1)).map(|x| { + let mut new_chunk = *x; + new_chunk.set_era(era); + new_chunk + }), + // Era is out of scope. + _ => None, + } + } } +// TODO: rename to SubperiodType? It would be less ambigious. /// Distinct period types in dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum PeriodType { @@ -273,6 +355,17 @@ impl PeriodInfo { } } +// TODO: doc +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct PeriodEndInfo { + #[codec(compact)] + pub bonus_reward_pool: Balance, + #[codec(compact)] + pub total_vp_stake: Balance, + #[codec(compact)] + pub final_era: EraNumber, +} + /// Force types to speed up the next era, and even period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum ForcingTypes { @@ -423,6 +516,9 @@ impl Default for StakeChunk { } impl AmountEraPair for StakeChunk { + fn new(amount: Balance, era: EraNumber) -> Self { + Self { amount, era } + } fn get_amount(&self) -> Balance { self.amount } @@ -693,6 +789,12 @@ where pub fn last_stake_era(&self) -> Option { self.staked.0.last().map(|chunk| chunk.era) } + + /// First entry for which a stake entry exists. + /// If no stake entries exist, returns `None`. + pub fn oldest_stake_era(&self) -> Option { + self.staked.0.first().map(|chunk| chunk.era) + } } // TODO: it would be nice to implement add/subtract logic on this struct and use it everywhere @@ -1289,7 +1391,8 @@ pub enum EraRewardSpanError { } /// Used to efficiently store era span information. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(SL))] pub struct EraRewardSpan> { /// Span of EraRewardInfo entries. span: BoundedVec, @@ -1331,7 +1434,7 @@ where /// `true` if span is empty, `false` otherwise. pub fn is_empty(&self) -> bool { - self.first_era == 0 && self.last_era == 0 + self.span.is_empty() } /// Push new `EraReward` entry into the span. @@ -1347,6 +1450,7 @@ where self.last_era = era; self.span .try_push(era_reward) + // Defensive check, should never happen since it means capacity is 'zero'. .map_err(|_| EraRewardSpanError::NoCapacity) } else { // Defensive check to ensure next era rewards refers to era after the last one in the span. @@ -1365,7 +1469,7 @@ where /// /// In case `era` is not covered by the span, `None` is returned. pub fn get(&self, era: EraNumber) -> Option<&EraReward> { - match self.first_era.checked_sub(era) { + match era.checked_sub(self.first_era()) { Some(index) => self.span.get(index as usize), None => None, } From fff06566538fc796c1110e762f898f022f730032 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 12 Oct 2023 21:56:20 +0200 Subject: [PATCH 03/86] Tests --- pallets/dapp-staking-v3/src/lib.rs | 12 +- pallets/dapp-staking-v3/src/test/mock.rs | 1 + .../dapp-staking-v3/src/test/tests_types.rs | 111 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 21 ++-- 4 files changed, 129 insertions(+), 16 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 65012f9bcd..8242cdf20c 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -321,7 +321,7 @@ pub mod pallet { /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. /// For the sake of simplicity, valid `era` keys are calculated as: /// - /// era_key = era - (era % T::EraRewardSpanLength) + /// era_key = era - (era % T::EraRewardSpanLength) /// /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. @@ -1008,7 +1008,6 @@ pub mod pallet { let era_rewards = EraRewards::::get(Self::era_reward_span_index(first_claim_era)) .ok_or(Error::::InternalClaimStakerError)?; - // TODO: need to know when period ended, storage item is needed for this. // last_period_era or current_era - 1 let last_period_era = if staked_period == protocol_state.period_number() { protocol_state.era.saturating_sub(1) @@ -1019,6 +1018,9 @@ pub mod pallet { }; let last_claim_era = era_rewards.last_era().min(last_period_era); + let is_full_period_claimed = + staked_period < protocol_state.period_number() && last_period_era == last_claim_era; + // Get chunks for reward claiming let chunks_for_claim = ledger .staked @@ -1045,7 +1047,11 @@ pub mod pallet { acc.saturating_add(*reward) }); - T::Currency::deposit_into_existing(&account, reward_sum); + // TODO; update & write ledger + if is_full_period_claimed {} + + T::Currency::deposit_into_existing(&account, reward_sum) + .map_err(|_| Error::::InternalClaimStakerError)?; rewards.into_iter().for_each(|(era, reward)| { Self::deposit_event(Event::::Reward { account: account.clone(), diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 398af7ab7d..bb287172a4 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -113,6 +113,7 @@ impl pallet_dapp_staking::Config for Test { type StandardErasPerVotingPeriod = ConstU32<8>; type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU16<10>; type MaxUnlockingChunks = ConstU32<5>; type MaxStakingChunks = ConstU32<8>; diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index bd5befa79b..ba59f03bb6 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -40,6 +40,9 @@ struct DummyEraAmount { era: u32, } impl AmountEraPair for DummyEraAmount { + fn new(amount: Balance, era: u32) -> Self { + Self { amount, era } + } fn get_amount(&self) -> Balance { self.amount } @@ -305,6 +308,7 @@ fn sparse_bounded_amount_era_vec_subtract_amount_advanced_non_consecutive_works( #[test] fn sparse_bounded_amount_era_vec_full_subtract_with_single_future_era() { get_u32_type!(MaxLen, 5); + let mut vec = SparseBoundedAmountEraVec::::new(); // A scenario where some amount is added, for the first time, for era X. @@ -320,6 +324,111 @@ fn sparse_bounded_amount_era_vec_full_subtract_with_single_future_era() { ); } +#[test] +fn sparse_bounded_amount_era_vec_split_left_works() { + get_u32_type!(MaxLen, 4); + + fn new_era_vec(vec: Vec) -> SparseBoundedAmountEraVec { + let vec: Vec = vec + .into_iter() + .map(|idx| DummyEraAmount::new(idx as Balance, idx)) + .collect(); + SparseBoundedAmountEraVec(BoundedVec::try_from(vec).unwrap()) + } + + // 1st scenario: [1,2,6,7] -- split(4) --> [1,2],[5,6,7] + let mut vec = new_era_vec(vec![1, 2, 6, 7]); + let result = vec.left_split(4).expect("Split should succeed."); + assert_eq!(result.0.len(), 2); + assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); + assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); + + assert_eq!(vec.0.len(), 3); + assert_eq!( + vec.0[0], + DummyEraAmount::new(2, 5), + "Amount must come from last entry in the split." + ); + assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); + assert_eq!(vec.0[2], DummyEraAmount::new(7, 7)); + + // 2nd scenario: [1,2] -- split(4) --> [1,2],[5] + let mut vec = new_era_vec(vec![1, 2]); + let result = vec.left_split(4).expect("Split should succeed."); + assert_eq!(result.0.len(), 2); + assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); + assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); + + assert_eq!(vec.0.len(), 1); + assert_eq!( + vec.0[0], + DummyEraAmount::new(2, 5), + "Amount must come from last entry in the split." + ); + + // 3rd scenario: [1,2,4,5] -- split(4) --> [1,2,4],[5] + let mut vec = new_era_vec(vec![1, 2, 4, 5]); + let result = vec.left_split(4).expect("Split should succeed."); + assert_eq!(result.0.len(), 3); + assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); + assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); + assert_eq!(result.0[2], DummyEraAmount::new(4, 4)); + + assert_eq!(vec.0.len(), 1); + assert_eq!(vec.0[0], DummyEraAmount::new(5, 5)); + + // 4th scenario: [1,2,4,6] -- split(4) --> [1,2,4],[5,6] + let mut vec = new_era_vec(vec![1, 2, 4, 6]); + let result = vec.left_split(4).expect("Split should succeed."); + assert_eq!(result.0.len(), 3); + assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); + assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); + assert_eq!(result.0[2], DummyEraAmount::new(4, 4)); + + assert_eq!(vec.0.len(), 2); + assert_eq!( + vec.0[0], + DummyEraAmount::new(4, 5), + "Amount must come from last entry in the split." + ); + assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); +} + +#[test] +fn sparse_bounded_amount_era_vec_split_left_fails_with_invalid_era() { + get_u32_type!(MaxLen, 4); + let mut vec = SparseBoundedAmountEraVec::::new(); + assert!(vec.add_amount(5, 5).is_ok()); + + assert_eq!(vec.left_split(4), Err(AccountLedgerError::SplitEraInvalid)); +} + +#[test] +fn sparse_bounded_amount_era_vec_get_works() { + get_u32_type!(MaxLen, 4); + let vec: Vec = vec![2, 3, 5] + .into_iter() + .map(|idx| DummyEraAmount::new(idx as Balance, idx)) + .collect(); + let vec = + SparseBoundedAmountEraVec::(BoundedVec::try_from(vec).unwrap()); + + assert_eq!(vec.get(1), None, "Era is not covered by the vector."); + assert_eq!(vec.get(2), Some(DummyEraAmount::new(2, 2))); + assert_eq!(vec.get(3), Some(DummyEraAmount::new(3, 3))); + assert_eq!( + vec.get(4), + Some(DummyEraAmount::new(3, 4)), + "Era is covered by the 3rd era." + ); + assert_eq!(vec.get(5), Some(DummyEraAmount::new(5, 5))); + assert_eq!( + vec.get(6), + Some(DummyEraAmount::new(5, 6)), + "Era is covered by the 5th era." + ); +} + #[test] fn period_type_sanity_check() { assert_eq!(PeriodType::Voting.next(), PeriodType::BuildAndEarn); @@ -1821,7 +1930,7 @@ fn era_reward_span_fails_when_expected() { // Attempting to push incorrect era results in an error for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { assert_eq!( - era_reward_span.push(era_1 - 1, era_reward), + era_reward_span.push(*wrong_era, era_reward), Err(EraRewardSpanError::InvalidEra) ); } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 849dbf4846..d8c8b37999 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -230,7 +230,6 @@ where Ok(()) } - // TODO: unit test /// Splits the vector into two parts, using the provided `era` as the splitting point. /// /// All entries which satisfy the condition `entry.era <= era` are removed from the vector. @@ -240,8 +239,8 @@ where /// The `era` argument **must** be contained within the vector's scope. /// /// E.g.: - /// a) [1,2,6,7] -- split(4) --> [1,2,4],[5,6,7] - /// b) [1,2] -- split(4) --> [1,2,4],[5] + /// a) [1,2,6,7] -- split(4) --> [1,2],[5,6,7] + /// b) [1,2] -- split(4) --> [1,2],[5] /// c) [1,2,4,5] -- split(4) --> [1,2,4],[5] /// d) [1,2,4,6] -- split(4) --> [1,2,4],[5,6] pub fn left_split(&mut self, era: EraNumber) -> Result { @@ -249,7 +248,7 @@ where // But I don't think it's worth doing, since we're aiming for the safe side here. // Split the inner vector into two parts - let (mut left, mut right): (Vec

, Vec

) = self + let (left, mut right): (Vec

, Vec

) = self .0 .clone() .into_iter() @@ -260,13 +259,6 @@ where } if let Some(&last_l_chunk) = left.last() { - // In case 'left' part is missing an entry covering the specified era, add it. - if last_l_chunk.get_era() < era { - let mut new_chunk = last_l_chunk; - new_chunk.set_era(era); - left.push(new_chunk); - } - // In case 'right' part is missing an entry covering the specified era, add it. match right.first() { Some(first_r_chunk) if first_r_chunk.get_era() > era.saturating_add(1) => { @@ -291,7 +283,6 @@ where )) } - // TODO: unit tests /// Returns the most appropriate chunk for the specified era, if it exists. pub fn get(&self, era: EraNumber) -> Option

{ match self.0.binary_search_by(|chunk| chunk.get_era().cmp(&era)) { @@ -795,6 +786,12 @@ where pub fn oldest_stake_era(&self) -> Option { self.staked.0.first().map(|chunk| chunk.era) } + + /// Notify ledger that all `stake` rewards have been claimed for the staked era. + pub fn all_stake_rewards_claimed(&mut self) { + self.staked = SparseBoundedAmountEraVec(BoundedVec::::default()); + self.staked_period = None; + } } // TODO: it would be nice to implement add/subtract logic on this struct and use it everywhere From 0daae3a68e65609fe2c443d731576766cfc87487 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 13 Oct 2023 11:42:04 +0200 Subject: [PATCH 04/86] Test utils for claim-staker --- pallets/dapp-staking-v3/src/lib.rs | 22 +-- pallets/dapp-staking-v3/src/test/mock.rs | 15 ++ .../dapp-staking-v3/src/test/testing_utils.rs | 136 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 1 + 4 files changed, 164 insertions(+), 10 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8242cdf20c..2fd93007e1 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -352,7 +352,7 @@ pub mod pallet { } let mut era_info = CurrentEraInfo::::get(); - let staker_reward_pool = Balance::from(1000_u128); // TODO: calculate this properly + let staker_reward_pool = Balance::from(1000_u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) let era_reward = EraReward::new(staker_reward_pool, era_info.total_staked_amount()); let ending_era = protocol_state.era; let next_era = ending_era.saturating_add(1); @@ -985,13 +985,17 @@ pub mod pallet { /// TODO #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] - pub fn claim_staker_reward(origin: OriginFor) -> DispatchResult { + pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; let protocol_state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); + // TODO: how do we handle expired rewards? Add an additional call to clean them up? + // Putting this logic inside existing calls will add even more complexity. + + // Check if the rewards have expired let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; ensure!( staked_period @@ -1026,9 +1030,11 @@ pub mod pallet { .staked .left_split(last_claim_era) .map_err(|_| Error::::InternalClaimStakerError)?; + ensure!(chunks_for_claim.0.len() > 0, Error::::NoClaimableRewards); // Calculate rewards let mut rewards: Vec<_> = Vec::new(); + let mut reward_sum = Balance::zero(); for era in first_claim_era..=last_claim_era { let era_reward = era_rewards .get(era) @@ -1041,14 +1047,14 @@ pub mod pallet { let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) * era_reward.staker_reward_pool(); rewards.push((era, staker_reward)); + reward_sum.saturating_accrue(staker_reward); } - let reward_sum = rewards.iter().fold(Balance::zero(), |acc, (_, reward)| { - acc.saturating_add(*reward) - }); - - // TODO; update & write ledger - if is_full_period_claimed {} + // Write updated ledger back to storage + if is_full_period_claimed { + ledger.all_stake_rewards_claimed(); + } + Self::update_ledger(&account, ledger); T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index bb287172a4..e2d6e1e185 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -235,3 +235,18 @@ pub(crate) fn _advance_to_next_period_type() { run_for_blocks(1); } } + +// Return all dApp staking events from the event buffer. +pub fn dapp_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let RuntimeEvent::DappStaking(inner) = e { + Some(inner) + } else { + None + } + }) + .collect() +} diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 532d856f05..0d6e1f0c8d 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -20,11 +20,12 @@ use crate::test::mock::*; use crate::types::*; use crate::{ pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, ContractStake, - CurrentEraInfo, DAppId, Event, IntegratedDApps, Ledger, NextDAppId, StakerInfo, + CurrentEraInfo, DAppId, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, + PeriodEndInfo, StakerInfo, }; use frame_support::{assert_ok, traits::Get}; -use sp_runtime::traits::Zero; +use sp_runtime::{traits::Zero, Perbill}; use std::collections::HashMap; /// Helper struct used to store the entire pallet state snapshot. @@ -48,6 +49,11 @@ pub(crate) struct MemorySnapshot { >, contract_stake: HashMap<::SmartContract, ContractStakingInfoSeries>, + era_rewards: HashMap< + EraNumber, + EraRewardSpan<::EraRewardSpanLength>, + >, + period_end: HashMap, } impl MemorySnapshot { @@ -63,6 +69,8 @@ impl MemorySnapshot { .map(|(k1, k2, v)| ((k1, k2), v)) .collect(), contract_stake: ContractStake::::iter().collect(), + era_rewards: EraRewards::::iter().collect(), + period_end: PeriodEnd::::iter().collect(), } } @@ -742,3 +750,127 @@ pub(crate) fn assert_unstake( ); } } + +/// Claim staker rewards. +pub(crate) fn assert_claim_staker_rewards(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(&account); + + // Get the first eligible era for claiming rewards + let first_claim_era = pre_ledger + .staked + .0 + .first() + .expect("Entry must exist, otherwise 'claim' is invalid.") + .get_era(); + + // Get the apprropriate era rewards span for the 'first era' + let era_span_length: EraNumber = + ::EraRewardSpanLength::get(); + let era_span_index = first_claim_era - (first_claim_era % era_span_length); + let era_rewards_span = pre_snapshot + .era_rewards + .get(&era_span_index) + .expect("Entry must exist, otherwise 'claim' is invalid."); + + // Calculate the final era for claiming rewards. Also determine if this will fully claim all staked period rewards. + let is_current_period_stake = match pre_ledger.staked_period { + Some(staked_period) + if staked_period == pre_snapshot.active_protocol_state.period_number() => + { + true + } + _ => false, + }; + + let (last_claim_era, is_full_claim) = if is_current_period_stake { + (pre_snapshot.active_protocol_state.era - 1, false) + } else { + let last_claim_era = era_rewards_span.last_era(); + + let claim_period = pre_ledger.staked_period.unwrap(); + let period_end = pre_snapshot + .period_end + .get(&claim_period) + .expect("Entry must exist, since it's a past period."); + + let last_claim_era = era_rewards_span.last_era().min(period_end.final_era); + let is_full_claim = last_claim_era == period_end.final_era; + (last_claim_era, is_full_claim) + }; + + assert!( + last_claim_era < pre_snapshot.active_protocol_state.era, + "Sanity check." + ); + + // Calculate the expected rewards + let mut rewards = Vec::new(); + for era in first_claim_era..=last_claim_era { + let era_reward_info = era_rewards_span + .get(era) + .expect("Entry must exist, otherwise 'claim' is invalid."); + let stake_chunk = pre_ledger + .staked + .get(era) + .expect("Entry must exist, otherwise 'claim' is invalid."); + + let reward = Perbill::from_rational(stake_chunk.amount, era_reward_info.staked()) + * era_reward_info.staker_reward_pool(); + rewards.push((era, reward)); + } + let total_reward = rewards + .iter() + .fold(Balance::zero(), |acc, (_, reward)| acc + reward); + + // Unstake from smart contract & verify event(s) + assert_ok!(DappStaking::claim_staker_rewards(RuntimeOrigin::signed( + account + ),)); + + let events = dapp_staking_events(); + assert_eq!(events.len(), rewards.len()); + for (event, (era, reward)) in events.iter().zip(rewards.iter()) { + assert_eq!( + event, + &Event::::Reward { + account, + era: *era, + amount: *reward, + } + ); + } + + // Verify post state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + total_reward, + "Total issuance must increase by the total reward amount." + ); + + let post_free_balance = ::Currency::free_balance(&account); + assert_eq!( + post_free_balance, + pre_free_balance + total_reward, + "Free balance must increase by the total reward amount." + ); + + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + + if is_full_claim { + assert!(post_ledger.staked.0.is_empty()); + assert!(post_ledger.staked_period.is_none()); + } else { + let stake_chunk = post_ledger.staked.0.first().expect("Entry must exist"); + assert_eq!(stake_chunk.era, last_claim_era + 1); + assert_eq!( + stake_chunk.amount, + pre_ledger.staked.get(last_claim_era).unwrap().amount + ); + } +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index d8c8b37999..875ece456c 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -789,6 +789,7 @@ where /// Notify ledger that all `stake` rewards have been claimed for the staked era. pub fn all_stake_rewards_claimed(&mut self) { + // TODO: improve handling once bonus reward tracking is added self.staked = SparseBoundedAmountEraVec(BoundedVec::::default()); self.staked_period = None; } From ebae6639d450de308aa36f83334bc1bd2fd9917b Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 13 Oct 2023 16:04:45 +0200 Subject: [PATCH 05/86] Bug fixes, improvements --- pallets/dapp-staking-v3/src/lib.rs | 62 ++++++++++++++----- .../dapp-staking-v3/src/test/testing_utils.rs | 56 ++++++++++++++++- pallets/dapp-staking-v3/src/test/tests.rs | 36 +++++++++++ pallets/dapp-staking-v3/src/types.rs | 29 ++++++++- 4 files changed, 163 insertions(+), 20 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 2fd93007e1..a966d632fd 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -352,10 +352,10 @@ pub mod pallet { } let mut era_info = CurrentEraInfo::::get(); - let staker_reward_pool = Balance::from(1000_u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) + let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) let era_reward = EraReward::new(staker_reward_pool, era_info.total_staked_amount()); - let ending_era = protocol_state.era; - let next_era = ending_era.saturating_add(1); + let current_era = protocol_state.era; + let next_era = current_era.saturating_add(1); let maybe_period_event = match protocol_state.period_type() { PeriodType::Voting => { // For the sake of consistency @@ -377,6 +377,17 @@ pub mod pallet { // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { + // Store info about period end + let bonus_reward_pool = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet + PeriodEnd::::insert( + &protocol_state.period_number(), + PeriodEndInfo { + bonus_reward_pool, + total_vp_stake: era_info.staked_amount(PeriodType::Voting), + final_era: current_era, + }, + ); + // For the sake of consistency we treat the whole `Voting` period as a single era. // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. let ending_era = next_era.saturating_add(1); @@ -411,12 +422,12 @@ pub mod pallet { CurrentEraInfo::::put(era_info); - let era_span_index = Self::era_reward_span_index(ending_era); + let era_span_index = Self::era_reward_span_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); // TODO: error must not happen here. Log an error if it does. // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. // Will require a fix from the runtime team though. - let _ = span.push(ending_era, era_reward); + let _ = span.push(current_era, era_reward); EraRewards::::insert(&era_span_index, span); Self::deposit_event(Event::::NewEra { era: next_era }); @@ -1006,13 +1017,14 @@ pub mod pallet { ); // Calculate the reward claim span - let first_claim_era = ledger - .oldest_stake_era() - .ok_or(Error::::InternalClaimStakerError)?; - let era_rewards = EraRewards::::get(Self::era_reward_span_index(first_claim_era)) + let (first_chunk, last_chunk) = ledger + .first_and_last_stake_chunks() .ok_or(Error::::InternalClaimStakerError)?; + let era_rewards = + EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) + .ok_or(Error::::InternalClaimStakerError)?; - // last_period_era or current_era - 1 + // The last era for which we can theoretically claim rewards. let last_period_era = if staked_period == protocol_state.period_number() { protocol_state.era.saturating_sub(1) } else { @@ -1020,10 +1032,20 @@ pub mod pallet { .ok_or(Error::::InternalClaimStakerError)? .final_era }; - let last_claim_era = era_rewards.last_era().min(last_period_era); - let is_full_period_claimed = - staked_period < protocol_state.period_number() && last_period_era == last_claim_era; + // The last era for which we can claim rewards for this account. + // Limiting factors are: + // 1. era reward span entries + // 2. last era in period limit + // 3. last era in which staker had non-zero stake + let last_claim_era = if last_chunk.get_amount().is_zero() { + era_rewards + .last_era() + .min(last_period_era) + .min(last_chunk.get_era()) + } else { + era_rewards.last_era().min(last_period_era) + }; // Get chunks for reward claiming let chunks_for_claim = ledger @@ -1032,10 +1054,17 @@ pub mod pallet { .map_err(|_| Error::::InternalClaimStakerError)?; ensure!(chunks_for_claim.0.len() > 0, Error::::NoClaimableRewards); + // Full reward claim is done if no more rewards are claimable. + // This can be either due to: 1) we've claimed the last period + // TODO: this is wrong, the first part of check needs to be improved + let is_full_period_claimed = staked_period < protocol_state.period_number() + && last_period_era == last_claim_era + || ledger.staked.0.is_empty(); + // Calculate rewards let mut rewards: Vec<_> = Vec::new(); let mut reward_sum = Balance::zero(); - for era in first_claim_era..=last_claim_era { + for era in first_chunk.get_era()..=last_claim_era { let era_reward = era_rewards .get(era) .ok_or(Error::::InternalClaimStakerError)?; @@ -1046,6 +1075,9 @@ pub mod pallet { let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) * era_reward.staker_reward_pool(); + if staker_reward.is_zero() { + continue; + } rewards.push((era, staker_reward)); reward_sum.saturating_accrue(staker_reward); } @@ -1123,7 +1155,7 @@ pub mod pallet { } /// Calculates the `EraRewardSpan` index for the specified era. - fn era_reward_span_index(era: EraNumber) -> EraNumber { + pub fn era_reward_span_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 0d6e1f0c8d..73043452a8 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -788,8 +788,6 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let (last_claim_era, is_full_claim) = if is_current_period_stake { (pre_snapshot.active_protocol_state.era - 1, false) } else { - let last_claim_era = era_rewards_span.last_era(); - let claim_period = pre_ledger.staked_period.unwrap(); let period_end = pre_snapshot .period_end @@ -825,6 +823,9 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { .iter() .fold(Balance::zero(), |acc, (_, reward)| acc + reward); + //clean up possible leftover events + System::reset_events(); + // Unstake from smart contract & verify event(s) assert_ok!(DappStaking::claim_staker_rewards(RuntimeOrigin::signed( account @@ -874,3 +875,54 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { ); } } + +/// Returns from which starting era to which ending era can rewards be claimed for the specified account. +/// +/// If `None` is returned, there is nothing to claim. +/// Doesn't consider reward expiration. +pub(crate) fn claimable_reward_range(account: AccountId) -> Option<(EraNumber, EraNumber)> { + let ledger = Ledger::::get(&account); + let protocol_state = ActiveProtocolState::::get(); + + let (first_chunk, last_chunk) = if let Some(chunks) = ledger.first_and_last_stake_chunks() { + chunks + } else { + return None; + }; + + // Full unstake happened, no rewards past this. + let last_claim_era = if last_chunk.get_amount().is_zero() { + last_chunk.get_era() - 1 + } else if ledger.staked_period == Some(protocol_state.period_number()) { + // Staked in the ongoing period, best we can do is claim up to last era + protocol_state.era - 1 + } else { + // Period finished, we can claim up to its final era + let period_end = PeriodEnd::::get(ledger.staked_period.unwrap()).unwrap(); + period_end.final_era + }; + + Some((first_chunk.get_era(), last_claim_era)) +} + +/// Number of times it's required to call `claim_staker_rewards` to claim all pending rewards. +/// +/// In case no rewards are pending, return **zero**. +pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { + let range = if let Some(range) = claimable_reward_range(account) { + range + } else { + return 0; + }; + + let era_span_length: EraNumber = + ::EraRewardSpanLength::get(); + let first = DappStaking::era_reward_span_index(range.0) + .checked_div(era_span_length) + .unwrap(); + let second = DappStaking::era_reward_span_index(range.1) + .checked_div(era_span_length) + .unwrap(); + + second - first + 1 +} diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 27f76a6d62..9fb269ce8a 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1235,3 +1235,39 @@ fn unstake_fails_due_to_too_many_chunks() { ); }) } + +#[test] +fn claim_staker_rewards_basic_example_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance into Build&Earn period, and allow one era to pass. Claim reward for 1 era. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_claim_staker_rewards(account); + + // Advance a few more eras, and claim multiple rewards this time. + advance_to_era(ActiveProtocolState::::get().era + 3); + assert_eq!( + ActiveProtocolState::::get().period_number(), + 1, + "Sanity check, we must still be in the 1st period." + ); + assert_claim_staker_rewards(account); + + // Advance into the next period, make sure we can still claim old rewards. + // TODO: I should calculate number of expected claims, so I know how much times `claim_staker_rewards` should be called. + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + }) +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 875ece456c..07afb13f6a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -243,6 +243,8 @@ where /// b) [1,2] -- split(4) --> [1,2],[5] /// c) [1,2,4,5] -- split(4) --> [1,2,4],[5] /// d) [1,2,4,6] -- split(4) --> [1,2,4],[5,6] + /// e) [1,2(0)] -- split(4) --> [1,2],[] // 2nd entry has 'zero' amount, so no need to keep it + // TODO: what if last element on the right is zero? We should cleanup the vector then. pub fn left_split(&mut self, era: EraNumber) -> Result { // TODO: this implementation can once again be optimized, sacrificing the code readability a bit. // But I don't think it's worth doing, since we're aiming for the safe side here. @@ -260,16 +262,24 @@ where if let Some(&last_l_chunk) = left.last() { // In case 'right' part is missing an entry covering the specified era, add it. - match right.first() { + let maybe_chunk = match right.first() { Some(first_r_chunk) if first_r_chunk.get_era() > era.saturating_add(1) => { let mut new_chunk = last_l_chunk.clone(); new_chunk.set_era(era.saturating_add(1)); - right.insert(0, new_chunk); + Some(new_chunk) } None => { let mut new_chunk = last_l_chunk.clone(); new_chunk.set_era(era.saturating_add(1)); - right.insert(0, new_chunk); + Some(new_chunk) + } + _ => None, + }; + + // Only insert the chunk if it's non-zero + match maybe_chunk { + Some(chunk) if chunk.get_amount() > Balance::zero() => { + right.insert(0, chunk); } _ => (), } @@ -775,18 +785,31 @@ where self.staked.subtract_amount(amount, era) } + // TODO: remove this /// Last era for which a stake entry exists. /// If no stake entries exist, returns `None`. pub fn last_stake_era(&self) -> Option { self.staked.0.last().map(|chunk| chunk.era) } + // TODO: remove this /// First entry for which a stake entry exists. /// If no stake entries exist, returns `None`. pub fn oldest_stake_era(&self) -> Option { self.staked.0.first().map(|chunk| chunk.era) } + // TODO + pub fn first_and_last_stake_chunks(&self) -> Option<(StakeChunk, StakeChunk)> { + let first = self.staked.0.first().map(|chunk| *chunk); + let last = self.staked.0.last().map(|chunk| *chunk); + + match (first, last) { + (Some(first), Some(last)) => Some((first, last)), + _ => None, + } + } + /// Notify ledger that all `stake` rewards have been claimed for the staked era. pub fn all_stake_rewards_claimed(&mut self) { // TODO: improve handling once bonus reward tracking is added From c76ade97e0f7f5453f249da22d6f6940931509b5 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 13 Oct 2023 22:21:02 +0200 Subject: [PATCH 06/86] Claim improvements & some tests --- pallets/dapp-staking-v3/src/lib.rs | 86 ++++++++++--------- pallets/dapp-staking-v3/src/test/mock.rs | 3 +- .../dapp-staking-v3/src/test/testing_utils.rs | 4 + pallets/dapp-staking-v3/src/test/tests.rs | 45 +++++++++- .../dapp-staking-v3/src/test/tests_types.rs | 13 +++ pallets/dapp-staking-v3/src/types.rs | 41 +++++++-- 6 files changed, 140 insertions(+), 52 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index a966d632fd..c703558aa9 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -352,13 +352,15 @@ pub mod pallet { } let mut era_info = CurrentEraInfo::::get(); - let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) - let era_reward = EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + let current_era = protocol_state.era; let next_era = current_era.saturating_add(1); - let maybe_period_event = match protocol_state.period_type() { + let (maybe_period_event, era_reward) = match protocol_state.period_type() { PeriodType::Voting => { - // For the sake of consistency + // For the sake of consistency, we put zero reward into storage + let era_reward = + EraReward::new(Balance::zero(), era_info.total_staked_amount()); + let ending_era = next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); let build_and_earn_start_block = @@ -367,13 +369,20 @@ pub mod pallet { era_info.migrate_to_next_era(Some(protocol_state.period_type())); - Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), - number: protocol_state.period_number(), - }) + ( + Some(Event::::NewPeriod { + period_type: protocol_state.period_type(), + number: protocol_state.period_number(), + }), + era_reward, + ) } PeriodType::BuildAndEarn => { - // TODO: trigger reward calculation here. This will be implemented later. + // 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 era_reward = + EraReward::new(staker_reward_pool, era_info.total_staked_amount()); // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { @@ -400,17 +409,20 @@ pub mod pallet { // TODO: trigger tier configuration calculation based on internal & external params. - Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), - number: protocol_state.period_number(), - }) + ( + Some(Event::::NewPeriod { + period_type: protocol_state.period_type(), + number: protocol_state.period_number(), + }), + era_reward, + ) } else { let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); protocol_state.next_era_start = next_era_start_block; era_info.migrate_to_next_era(None); - None + (None, era_reward) } } }; @@ -1022,15 +1034,16 @@ pub mod pallet { .ok_or(Error::::InternalClaimStakerError)?; let era_rewards = EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) - .ok_or(Error::::InternalClaimStakerError)?; + .ok_or(Error::::NoClaimableRewards)?; // The last era for which we can theoretically claim rewards. - let last_period_era = if staked_period == protocol_state.period_number() { - protocol_state.era.saturating_sub(1) + // And indicator if we know the period's ending era. + let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { + (protocol_state.era.saturating_sub(1), None) } else { PeriodEnd::::get(&staked_period) + .map(|info| (info.final_era, Some(info.final_era))) .ok_or(Error::::InternalClaimStakerError)? - .final_era }; // The last era for which we can claim rewards for this account. @@ -1048,23 +1061,19 @@ pub mod pallet { }; // Get chunks for reward claiming - let chunks_for_claim = ledger - .staked - .left_split(last_claim_era) - .map_err(|_| Error::::InternalClaimStakerError)?; - ensure!(chunks_for_claim.0.len() > 0, Error::::NoClaimableRewards); - - // Full reward claim is done if no more rewards are claimable. - // This can be either due to: 1) we've claimed the last period - // TODO: this is wrong, the first part of check needs to be improved - let is_full_period_claimed = staked_period < protocol_state.period_number() - && last_period_era == last_claim_era - || ledger.staked.0.is_empty(); + let chunks_for_claim = + ledger + .claim_up_to_era(last_claim_era, period_end) + .map_err(|err| match err { + AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, + _ => Error::::InternalClaimStakerError, + })?; // Calculate rewards let mut rewards: Vec<_> = Vec::new(); let mut reward_sum = Balance::zero(); for era in first_chunk.get_era()..=last_claim_era { + // TODO: this should be zipped, and values should be fetched only once let era_reward = era_rewards .get(era) .ok_or(Error::::InternalClaimStakerError)?; @@ -1073,23 +1082,22 @@ pub mod pallet { .get(era) .ok_or(Error::::InternalClaimStakerError)?; - let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) - * era_reward.staker_reward_pool(); - if staker_reward.is_zero() { + // Optimization, and zero-division protection + if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { continue; } + let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) + * era_reward.staker_reward_pool(); + rewards.push((era, staker_reward)); reward_sum.saturating_accrue(staker_reward); } - // Write updated ledger back to storage - if is_full_period_claimed { - ledger.all_stake_rewards_claimed(); - } - Self::update_ledger(&account, ledger); - T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; + + Self::update_ledger(&account, ledger); + rewards.into_iter().for_each(|(era, reward)| { Self::deposit_event(Event::::Reward { account: account.clone(), diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index e2d6e1e185..1cd94100b3 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -155,7 +155,6 @@ impl ExtBuilder { let mut ext = TestExternalities::from(storage); ext.execute_with(|| { System::set_block_number(1); - DappStaking::on_initialize(System::block_number()); // TODO: not sure why the mess with type happens here, I can check it later let era_length: BlockNumber = @@ -175,6 +174,8 @@ impl ExtBuilder { }, maintenance: false, }); + + // DappStaking::on_initialize(System::block_number()); }); ext diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 73043452a8..ccc171a46b 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -817,6 +817,10 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let reward = Perbill::from_rational(stake_chunk.amount, era_reward_info.staked()) * era_reward_info.staker_reward_pool(); + if reward.is_zero() { + continue; + } + rewards.push((era, reward)); } let total_reward = rewards diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 9fb269ce8a..56fe48118f 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -19,8 +19,8 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, EraNumber, Error, IntegratedDApps, - Ledger, NextDAppId, PeriodType, StakerInfo, + pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, + IntegratedDApps, Ledger, NextDAppId, PeriodType, StakerInfo, }; use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; @@ -1264,10 +1264,49 @@ fn claim_staker_rewards_basic_example_is_ok() { assert_claim_staker_rewards(account); // Advance into the next period, make sure we can still claim old rewards. - // TODO: I should calculate number of expected claims, so I know how much times `claim_staker_rewards` should be called. advance_to_next_period(); for _ in 0..required_number_of_reward_claims(account) { assert_claim_staker_rewards(account); } }) } + +#[test] +fn claim_staker_rewards_no_claimable_rewards_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // 1st scenario - try to claim with no stake at all. + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 2nd scenario - stake some amount, and try to claim in the same era. + // It's important this is the 1st era, when no `EraRewards` entry exists. + assert_eq!(ActiveProtocolState::::get().era, 1, "Sanity check"); + assert!(EraRewards::::iter().next().is_none(), "Sanity check"); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 3rd scenario - move over to the next era, but we still expect failure because + // stake is valid from era 2 (current era), and we're trying to claim rewards for era 1. + advance_to_next_era(); + assert!(EraRewards::::iter().next().is_some(), "Sanity check"); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index ba59f03bb6..dd301b983d 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -392,6 +392,19 @@ fn sparse_bounded_amount_era_vec_split_left_works() { "Amount must come from last entry in the split." ); assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); + + // 5th scenario: [1,2(0)] -- split(4) --> [1,2],[] + let vec: Vec = vec![(1, 1), (0, 2)] + .into_iter() + .map(|(amount, era)| DummyEraAmount::new(amount as Balance, era)) + .collect(); + let mut vec = SparseBoundedAmountEraVec::<_, MaxLen>(BoundedVec::try_from(vec).unwrap()); + let result = vec.left_split(4).expect("Split should succeed."); + assert_eq!(result.0.len(), 2); + assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); + assert_eq!(result.0[1], DummyEraAmount::new(0, 2)); + + assert!(vec.0.is_empty()); } #[test] diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 07afb13f6a..267185bd9f 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -244,11 +244,7 @@ where /// c) [1,2,4,5] -- split(4) --> [1,2,4],[5] /// d) [1,2,4,6] -- split(4) --> [1,2,4],[5,6] /// e) [1,2(0)] -- split(4) --> [1,2],[] // 2nd entry has 'zero' amount, so no need to keep it - // TODO: what if last element on the right is zero? We should cleanup the vector then. pub fn left_split(&mut self, era: EraNumber) -> Result { - // TODO: this implementation can once again be optimized, sacrificing the code readability a bit. - // But I don't think it's worth doing, since we're aiming for the safe side here. - // Split the inner vector into two parts let (left, mut right): (Vec

, Vec

) = self .0 @@ -810,11 +806,38 @@ where } } - /// Notify ledger that all `stake` rewards have been claimed for the staked era. - pub fn all_stake_rewards_claimed(&mut self) { - // TODO: improve handling once bonus reward tracking is added - self.staked = SparseBoundedAmountEraVec(BoundedVec::::default()); - self.staked_period = None; + /// Claim up stake chunks up to the specified `era`. + /// Returns the vector describing claimable chunks. + /// + /// If `period_end` is provided, it's used to determine whether all applicable chunks have been claimed. + pub fn claim_up_to_era( + &mut self, + era: EraNumber, + period_end: Option, + ) -> Result, AccountLedgerError> { + let claim_chunks = self.staked.left_split(era)?; + + // Check if all possible chunks have been claimed + match self.staked.0.first() { + Some(chunk) => { + match period_end { + // If first chunk is after the period end, clearly everything has been claimed + Some(period_end) if chunk.get_era() > period_end => { + self.staked_period = None; + self.staked = SparseBoundedAmountEraVec::new(); + } + _ => (), + } + } + // No more chunks remain, meaning everything has been claimed + None => { + self.staked_period = None; + } + } + + // TODO: this is a bit clunky - introduce variables instead that keep track whether rewards were claimed or not. + + Ok(claim_chunks) } } From 096726eb4e716471ce14e88847897b4e1ddbb9d7 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 16 Oct 2023 16:46:12 +0200 Subject: [PATCH 07/86] Refactoring in progress --- pallets/dapp-staking-v3/src/lib.rs | 2214 ++++++------- pallets/dapp-staking-v3/src/test/mock.rs | 424 +-- pallets/dapp-staking-v3/src/test/mod.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 2779 +++++++---------- pallets/dapp-staking-v3/src/types.rs | 792 ++--- 5 files changed, 2713 insertions(+), 3500 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index c703558aa9..4baa8e6b15 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -53,7 +53,7 @@ use sp_runtime::{ use astar_primitives::Balance; use crate::types::*; -pub use pallet::*; +// pub use pallet::*; #[cfg(test)] mod test; @@ -62,1109 +62,1109 @@ mod types; const STAKING_ID: LockIdentifier = *b"dapstake"; -#[frame_support::pallet] -pub mod pallet { - use super::*; - - /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: frame_system::Config { - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - /// Currency used for staking. - /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) - type Currency: LockableCurrency< - Self::AccountId, - Moment = Self::BlockNumber, - Balance = Balance, - >; - - /// Describes smart contract in the context required by dApp staking. - type SmartContract: Parameter + Member + MaxEncodedLen; - - /// Privileged origin for managing dApp staking pallet. - type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; - - /// Length of a standard era in block numbers. - #[pallet::constant] - type StandardEraLength: Get; - - /// Length of the `Voting` period in standard eras. - /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras - /// for the sake of simplicity & consistency. - #[pallet::constant] - type StandardErasPerVotingPeriod: Get; - - /// Length of the `Build&Earn` period in standard eras. - /// Each `Build&Earn` period consists of one or more distinct standard eras. - #[pallet::constant] - type StandardErasPerBuildAndEarnPeriod: Get; - - /// Maximum length of a single era reward span length entry. - #[pallet::constant] - type EraRewardSpanLength: Get; - - /// Number of periods for which we keep rewards available for claiming. - /// After that period, they are no longer claimable. - #[pallet::constant] - type RewardRetentionInPeriods: Get; - - /// Maximum number of contracts that can be integrated into dApp staking at once. - #[pallet::constant] - type MaxNumberOfContracts: Get; - - /// Maximum number of unlocking chunks that can exist per account at a time. - #[pallet::constant] - type MaxUnlockingChunks: Get; - - /// Minimum amount an account has to lock in dApp staking in order to participate. - #[pallet::constant] - type MinimumLockedAmount: Get; - - /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. - #[pallet::constant] - type UnlockingPeriod: Get>; - - /// Maximum number of staking chunks that can exist per account at a time. - #[pallet::constant] - type MaxStakingChunks: Get; - - /// Minimum amount staker can stake on a contract. - #[pallet::constant] - type MinimumStakeAmount: Get; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(crate) fn deposit_event)] - pub enum Event { - /// New era has started. - NewEra { era: EraNumber }, - /// New period has started. - NewPeriod { - period_type: PeriodType, - number: PeriodNumber, - }, - /// A smart contract has been registered for dApp staking - DAppRegistered { - owner: T::AccountId, - smart_contract: T::SmartContract, - dapp_id: DAppId, - }, - /// dApp reward destination has been updated. - DAppRewardDestinationUpdated { - smart_contract: T::SmartContract, - beneficiary: Option, - }, - /// dApp owner has been changed. - DAppOwnerChanged { - smart_contract: T::SmartContract, - new_owner: T::AccountId, - }, - /// dApp has been unregistered - DAppUnregistered { - smart_contract: T::SmartContract, - era: EraNumber, - }, - /// Account has locked some amount into dApp staking. - Locked { - account: T::AccountId, - amount: Balance, - }, - /// Account has started the unlocking process for some amount. - Unlocking { - account: T::AccountId, - amount: Balance, - }, - /// Account has claimed unlocked amount, removing the lock from it. - ClaimedUnlocked { - account: T::AccountId, - amount: Balance, - }, - /// Account has relocked all of the unlocking chunks. - Relock { - account: T::AccountId, - amount: Balance, - }, - /// Account has staked some amount on a smart contract. - Stake { - account: T::AccountId, - smart_contract: T::SmartContract, - amount: Balance, - }, - /// Account has unstaked some amount from a smart contract. - Unstake { - account: T::AccountId, - smart_contract: T::SmartContract, - amount: Balance, - }, - /// Account has claimed some stake rewards. - Reward { - account: T::AccountId, - era: EraNumber, - amount: Balance, - }, - } - - #[pallet::error] - pub enum Error { - /// Pallet is disabled/in maintenance mode. - Disabled, - /// Smart contract already exists within dApp staking protocol. - ContractAlreadyExists, - /// Maximum number of smart contracts has been reached. - ExceededMaxNumberOfContracts, - /// Not possible to assign a new dApp Id. - /// This should never happen since current type can support up to 65536 - 1 unique dApps. - NewDAppIdUnavailable, - /// Specified smart contract does not exist in dApp staking. - ContractNotFound, - /// Call origin is not dApp owner. - OriginNotOwner, - /// dApp is part of dApp staking but isn't active anymore. - NotOperatedDApp, - /// Performing locking or staking with 0 amount. - ZeroAmount, - /// Total locked amount for staker is below minimum threshold. - LockedAmountBelowThreshold, - /// Cannot add additional locked balance chunks due to capacity limit. - TooManyLockedBalanceChunks, - /// Cannot add additional unlocking chunks due to capacity limit. - TooManyUnlockingChunks, - /// Remaining stake prevents entire balance of starting the unlocking process. - RemainingStakePreventsFullUnlock, - /// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all. - NoUnlockedChunksToClaim, - /// There are no unlocking chunks available to relock. - NoUnlockingChunks, - /// The amount being staked is too large compared to what's available for staking. - UnavailableStakeFunds, - /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. - UnclaimedRewardsFromPastPeriods, - /// Cannot add additional stake chunks due to capacity limit. - TooManyStakeChunks, - /// An unexpected error occured while trying to stake. - InternalStakeError, - /// Total staked amount on contract is below the minimum required value. - InsufficientStakeAmount, - /// Stake operation is rejected since period ends in the next era. - PeriodEndsInNextEra, - /// Unstaking is rejected since the period in which past stake was active has passed. - UnstakeFromPastPeriod, - /// Unstake amount is greater than the staked amount. - UnstakeAmountTooLarge, - /// Account has no staking information for the contract. - NoStakingInfo, - /// An unexpected error occured while trying to unstake. - InternalUnstakeError, - /// Rewards are no longer claimable since they are too old. - StakerRewardsExpired, - /// There are no claimable rewards for the account. - NoClaimableRewards, - /// An unexpected error occured while trying to claim rewards. - InternalClaimStakerError, - } - - /// General information about dApp staking protocol state. - #[pallet::storage] - pub type ActiveProtocolState = - StorageValue<_, ProtocolState>, ValueQuery>; - - /// Counter for unique dApp identifiers. - #[pallet::storage] - pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; - - /// Map of all dApps integrated into dApp staking protocol. - #[pallet::storage] - pub type IntegratedDApps = CountedStorageMap< - _, - Blake2_128Concat, - T::SmartContract, - DAppInfo, - OptionQuery, - >; - - /// General locked/staked information for each account. - #[pallet::storage] - pub type Ledger = - StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; - - /// Information about how much each staker has staked for each smart contract in some period. - #[pallet::storage] - pub type StakerInfo = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, - Blake2_128Concat, - T::SmartContract, - SingularStakingInfo, - OptionQuery, - >; - - /// Information about how much has been staked on a smart contract in some era or period. - #[pallet::storage] - pub type ContractStake = - StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakingInfoSeries, ValueQuery>; - - /// General information about the current era. - #[pallet::storage] - pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; - - /// Information about rewards for each era. - /// - /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. - /// For the sake of simplicity, valid `era` keys are calculated as: - /// - /// era_key = era - (era % T::EraRewardSpanLength) - /// - /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. - /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. - #[pallet::storage] - pub type EraRewards = - StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan, OptionQuery>; - - /// Information about period's end. - #[pallet::storage] - pub type PeriodEnd = - StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; - - #[pallet::hooks] - impl Hooks> for Pallet { - fn on_initialize(now: BlockNumberFor) -> Weight { - let mut protocol_state = ActiveProtocolState::::get(); - - // We should not modify pallet storage while in maintenance mode. - // This is a safety measure, since maintenance mode is expected to be - // enabled in case some misbehavior or corrupted storage is detected. - if protocol_state.maintenance { - return T::DbWeight::get().reads(1); - } - - // Nothing to do if it's not new era - if !protocol_state.is_new_era(now) { - return T::DbWeight::get().reads(1); - } - - let mut era_info = CurrentEraInfo::::get(); - - let current_era = protocol_state.era; - let next_era = current_era.saturating_add(1); - let (maybe_period_event, era_reward) = match protocol_state.period_type() { - PeriodType::Voting => { - // For the sake of consistency, we put zero reward into storage - let era_reward = - EraReward::new(Balance::zero(), era_info.total_staked_amount()); - - let ending_era = - next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); - let build_and_earn_start_block = - now.saturating_add(T::StandardEraLength::get()); - protocol_state.next_period_type(ending_era, build_and_earn_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.period_type())); - - ( - Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), - number: protocol_state.period_number(), - }), - era_reward, - ) - } - 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 era_reward = - EraReward::new(staker_reward_pool, era_info.total_staked_amount()); - - // Switch to `Voting` period if conditions are met. - if protocol_state.period_info.is_next_period(next_era) { - // Store info about period end - let bonus_reward_pool = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet - PeriodEnd::::insert( - &protocol_state.period_number(), - PeriodEndInfo { - bonus_reward_pool, - total_vp_stake: era_info.staked_amount(PeriodType::Voting), - final_era: current_era, - }, - ); - - // For the sake of consistency we treat the whole `Voting` period as a single era. - // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. - let ending_era = next_era.saturating_add(1); - let voting_period_length = Self::blocks_per_voting_period(); - let next_era_start_block = now.saturating_add(voting_period_length); - - protocol_state.next_period_type(ending_era, next_era_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.period_type())); - - // TODO: trigger tier configuration calculation based on internal & external params. - - ( - Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), - number: protocol_state.period_number(), - }), - era_reward, - ) - } else { - let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.next_era_start = next_era_start_block; - - era_info.migrate_to_next_era(None); - - (None, era_reward) - } - } - }; - - // Update storage items - - protocol_state.era = next_era; - ActiveProtocolState::::put(protocol_state); - - CurrentEraInfo::::put(era_info); - - let era_span_index = Self::era_reward_span_index(current_era); - let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); - // TODO: error must not happen here. Log an error if it does. - // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. - // Will require a fix from the runtime team though. - let _ = span.push(current_era, era_reward); - EraRewards::::insert(&era_span_index, span); - - Self::deposit_event(Event::::NewEra { era: next_era }); - if let Some(period_event) = maybe_period_event { - Self::deposit_event(period_event); - } - - // TODO: benchmark later - T::DbWeight::get().reads_writes(3, 3) - } - } - - #[pallet::call] - impl Pallet { - /// Used to enable or disable maintenance mode. - /// Can only be called by manager origin. - #[pallet::call_index(0)] - #[pallet::weight(Weight::zero())] - pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { - T::ManagerOrigin::ensure_origin(origin)?; - ActiveProtocolState::::mutate(|state| state.maintenance = enabled); - Ok(()) - } - - /// Used to register a new contract for dApp staking. - /// - /// If successful, smart contract will be assigned a simple, unique numerical identifier. - #[pallet::call_index(1)] - #[pallet::weight(Weight::zero())] - pub fn register( - origin: OriginFor, - owner: T::AccountId, - smart_contract: T::SmartContract, - ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - T::ManagerOrigin::ensure_origin(origin)?; - - ensure!( - !IntegratedDApps::::contains_key(&smart_contract), - Error::::ContractAlreadyExists, - ); - - ensure!( - IntegratedDApps::::count() < T::MaxNumberOfContracts::get().into(), - Error::::ExceededMaxNumberOfContracts - ); - - let dapp_id = NextDAppId::::get(); - // MAX value must never be assigned as a dApp Id since it serves as a sentinel value. - ensure!(dapp_id < DAppId::MAX, Error::::NewDAppIdUnavailable); - - IntegratedDApps::::insert( - &smart_contract, - DAppInfo { - owner: owner.clone(), - id: dapp_id, - state: DAppState::Registered, - reward_destination: None, - }, - ); - - NextDAppId::::put(dapp_id.saturating_add(1)); - - Self::deposit_event(Event::::DAppRegistered { - owner, - smart_contract, - dapp_id, - }); - - Ok(()) - } - - /// Used to modify the reward destination account for a dApp. - /// - /// Caller has to be dApp owner. - /// If set to `None`, rewards will be deposited to the dApp owner. - #[pallet::call_index(2)] - #[pallet::weight(Weight::zero())] - pub fn set_dapp_reward_destination( - origin: OriginFor, - smart_contract: T::SmartContract, - beneficiary: Option, - ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let dev_account = ensure_signed(origin)?; - - IntegratedDApps::::try_mutate( - &smart_contract, - |maybe_dapp_info| -> DispatchResult { - let dapp_info = maybe_dapp_info - .as_mut() - .ok_or(Error::::ContractNotFound)?; - - ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner); - - dapp_info.reward_destination = beneficiary.clone(); - - Ok(()) - }, - )?; - - Self::deposit_event(Event::::DAppRewardDestinationUpdated { - smart_contract, - beneficiary, - }); - - Ok(()) - } - - /// Used to change dApp owner. - /// - /// Can be called by dApp owner or dApp staking manager origin. - /// This is useful in two cases: - /// 1. when the dApp owner account is compromised, manager can change the owner to a new account - /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). - #[pallet::call_index(3)] - #[pallet::weight(Weight::zero())] - pub fn set_dapp_owner( - origin: OriginFor, - smart_contract: T::SmartContract, - new_owner: T::AccountId, - ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let origin = Self::ensure_signed_or_manager(origin)?; - - IntegratedDApps::::try_mutate( - &smart_contract, - |maybe_dapp_info| -> DispatchResult { - let dapp_info = maybe_dapp_info - .as_mut() - .ok_or(Error::::ContractNotFound)?; - - // If manager origin, `None`, no need to check if caller is the owner. - if let Some(caller) = origin { - ensure!(dapp_info.owner == caller, Error::::OriginNotOwner); - } - - dapp_info.owner = new_owner.clone(); - - Ok(()) - }, - )?; - - Self::deposit_event(Event::::DAppOwnerChanged { - smart_contract, - new_owner, - }); - - Ok(()) - } - - /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. - /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. - /// - /// Can be called by dApp owner or dApp staking manager origin. - #[pallet::call_index(4)] - #[pallet::weight(Weight::zero())] - pub fn unregister( - origin: OriginFor, - smart_contract: T::SmartContract, - ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - T::ManagerOrigin::ensure_origin(origin)?; - - let current_era = ActiveProtocolState::::get().era; - - IntegratedDApps::::try_mutate( - &smart_contract, - |maybe_dapp_info| -> DispatchResult { - let dapp_info = maybe_dapp_info - .as_mut() - .ok_or(Error::::ContractNotFound)?; - - ensure!( - dapp_info.state == DAppState::Registered, - Error::::NotOperatedDApp - ); - - dapp_info.state = DAppState::Unregistered(current_era); - - Ok(()) - }, - )?; - - // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. - - // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' - - // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' - - Self::deposit_event(Event::::DAppUnregistered { - smart_contract, - era: current_era, - }); - - Ok(()) - } - - /// Locks additional funds into dApp staking. - /// - /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. - /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. - /// - /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, - /// caller should claim pending rewards, before retrying to lock additional funds. - #[pallet::call_index(5)] - #[pallet::weight(Weight::zero())] - pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - let mut ledger = Ledger::::get(&account); - - // Calculate & check amount available for locking - let available_balance = - T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); - let amount_to_lock = available_balance.min(amount); - ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); - - ledger.add_lock_amount(amount_to_lock); - - ensure!( - ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), - Error::::LockedAmountBelowThreshold - ); - - Self::update_ledger(&account, ledger); - CurrentEraInfo::::mutate(|era_info| { - era_info.add_locked(amount_to_lock); - }); - - Self::deposit_event(Event::::Locked { - account, - amount: amount_to_lock, - }); - - Ok(()) - } - - /// Attempts to start the unlocking process for the specified amount. - /// - /// Only the amount that isn't actively used for staking can be unlocked. - /// If the amount is greater than the available amount for unlocking, everything is unlocked. - /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. - #[pallet::call_index(6)] - #[pallet::weight(Weight::zero())] - pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - let state = ActiveProtocolState::::get(); - let mut ledger = Ledger::::get(&account); - - let available_for_unlocking = ledger.unlockable_amount(state.period_info.number); - let amount_to_unlock = available_for_unlocking.min(amount); - - // Ensure we unlock everything if remaining amount is below threshold. - let remaining_amount = ledger - .active_locked_amount() - .saturating_sub(amount_to_unlock); - let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { - ensure!( - ledger.active_stake(state.period_info.number).is_zero(), - Error::::RemainingStakePreventsFullUnlock - ); - ledger.active_locked_amount() - } else { - amount_to_unlock - }; - - // Sanity check - ensure!(!amount_to_unlock.is_zero(), Error::::ZeroAmount); - - // Update ledger with new lock and unlocking amounts - ledger.subtract_lock_amount(amount_to_unlock); - - let current_block = frame_system::Pallet::::block_number(); - let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); - ledger - .add_unlocking_chunk(amount_to_unlock, unlock_block) - .map_err(|_| Error::::TooManyUnlockingChunks)?; - - // Update storage - Self::update_ledger(&account, ledger); - CurrentEraInfo::::mutate(|era_info| { - era_info.unlocking_started(amount_to_unlock); - }); - - Self::deposit_event(Event::::Unlocking { - account, - amount: amount_to_unlock, - }); - - Ok(()) - } - - /// Claims all of fully unlocked chunks, removing the lock from them. - #[pallet::call_index(7)] - #[pallet::weight(Weight::zero())] - pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - let mut ledger = Ledger::::get(&account); - - let current_block = frame_system::Pallet::::block_number(); - let amount = ledger.claim_unlocked(current_block); - ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); - - Self::update_ledger(&account, ledger); - CurrentEraInfo::::mutate(|era_info| { - era_info.unlocking_removed(amount); - }); - - // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) - - // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have - - Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); - - Ok(()) - } - - #[pallet::call_index(8)] - #[pallet::weight(Weight::zero())] - pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - let mut ledger = Ledger::::get(&account); - - ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); - - let amount = ledger.consume_unlocking_chunks(); - - ledger.add_lock_amount(amount); - ensure!( - ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), - Error::::LockedAmountBelowThreshold - ); - - Self::update_ledger(&account, ledger); - CurrentEraInfo::::mutate(|era_info| { - era_info.add_locked(amount); - era_info.unlocking_removed(amount); - }); - - Self::deposit_event(Event::::Relock { account, amount }); - - Ok(()) - } - - /// Stake the specified amount on a smart contract. - /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. - /// - /// Depending on the period type, appropriate stake amount will be updated. - #[pallet::call_index(9)] - #[pallet::weight(Weight::zero())] - pub fn stake( - origin: OriginFor, - smart_contract: T::SmartContract, - #[pallet::compact] amount: Balance, - ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - ensure!(amount > 0, Error::::ZeroAmount); - - ensure!( - Self::is_active(&smart_contract), - Error::::NotOperatedDApp - ); - - let protocol_state = ActiveProtocolState::::get(); - // Staker always stakes from the NEXT era - let stake_era = protocol_state.era.saturating_add(1); - ensure!( - !protocol_state.period_info.is_next_period(stake_era), - Error::::PeriodEndsInNextEra - ); - - let mut ledger = Ledger::::get(&account); - - // 1. - // Increase stake amount for the next era & current period in staker's ledger - ledger - .add_stake_amount(amount, stake_era, protocol_state.period_number()) - .map_err(|err| match err { - AccountLedgerError::InvalidPeriod => { - Error::::UnclaimedRewardsFromPastPeriods - } - AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, - AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, - // Defensive check, should never happen - _ => Error::::InternalStakeError, - })?; - - // 2. - // Update `StakerInfo` storage with the new stake amount on the specified contract. - // - // There are two distinct scenarios: - // 1. Existing entry matches the current period number - just update it. - // 2. Entry doesn't exist or it's for an older period - create a new one. - // - // This is ok since we only use this storage entry to keep track of how much each staker - // has staked on each contract in the current period. We only ever need the latest information. - // This is because `AccountLedger` is the one keeping information about how much was staked when. - let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { - Some(mut staking_info) - if staking_info.period_number() == protocol_state.period_number() => - { - staking_info.stake(amount, protocol_state.period_info.period_type); - staking_info - } - _ => { - ensure!( - amount >= T::MinimumStakeAmount::get(), - Error::::InsufficientStakeAmount - ); - let mut staking_info = SingularStakingInfo::new( - protocol_state.period_info.number, - protocol_state.period_info.period_type, - ); - staking_info.stake(amount, protocol_state.period_info.period_type); - staking_info - } - }; - - // 3. - // Update `ContractStake` storage with the new stake amount on the specified contract. - let mut contract_stake_info = ContractStake::::get(&smart_contract); - ensure!( - contract_stake_info - .stake(amount, protocol_state.period_info, stake_era) - .is_ok(), - Error::::InternalStakeError - ); - - // 4. - // Update total staked amount for the next era. - CurrentEraInfo::::mutate(|era_info| { - era_info.add_stake_amount(amount, protocol_state.period_type()); - }); - - // 5. - // Update remaining storage entries - Self::update_ledger(&account, ledger); - StakerInfo::::insert(&account, &smart_contract, new_staking_info); - ContractStake::::insert(&smart_contract, contract_stake_info); - - Self::deposit_event(Event::::Stake { - account, - smart_contract, - amount, - }); - - Ok(()) - } - - /// Unstake the specified amount from a smart contract. - /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. - /// - /// Depending on the period type, appropriate stake amount will be updated. - #[pallet::call_index(10)] - #[pallet::weight(Weight::zero())] - pub fn unstake( - origin: OriginFor, - smart_contract: T::SmartContract, - #[pallet::compact] amount: Balance, - ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - ensure!(amount > 0, Error::::ZeroAmount); - - ensure!( - Self::is_active(&smart_contract), - Error::::NotOperatedDApp - ); - - let protocol_state = ActiveProtocolState::::get(); - let unstake_era = protocol_state.era; - - let mut ledger = Ledger::::get(&account); - - // 1. - // Update `StakerInfo` storage with the reduced stake amount on the specified contract. - let (new_staking_info, amount) = match StakerInfo::::get(&account, &smart_contract) { - Some(mut staking_info) => { - ensure!( - staking_info.period_number() == protocol_state.period_number(), - Error::::UnstakeFromPastPeriod - ); - ensure!( - staking_info.total_staked_amount() >= amount, - Error::::UnstakeAmountTooLarge - ); - - // If unstaking would take the total staked amount below the minimum required value, - // unstake everything. - let amount = if staking_info.total_staked_amount().saturating_sub(amount) - < T::MinimumStakeAmount::get() - { - staking_info.total_staked_amount() - } else { - amount - }; - - staking_info.unstake(amount, protocol_state.period_type()); - (staking_info, amount) - } - None => { - return Err(Error::::NoStakingInfo.into()); - } - }; - - // 2. - // Reduce stake amount - ledger - .unstake_amount(amount, unstake_era, protocol_state.period_number()) - .map_err(|err| match err { - AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, - AccountLedgerError::UnstakeAmountLargerThanStake => { - Error::::UnstakeAmountTooLarge - } - AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, - _ => Error::::InternalUnstakeError, - })?; - - // 3. - // Update `ContractStake` storage with the reduced stake amount on the specified contract. - let mut contract_stake_info = ContractStake::::get(&smart_contract); - ensure!( - contract_stake_info - .unstake(amount, protocol_state.period_info, unstake_era) - .is_ok(), - Error::::InternalUnstakeError - ); - - // 4. - // Update total staked amount for the next era. - CurrentEraInfo::::mutate(|era_info| { - era_info.unstake_amount(amount, protocol_state.period_type()); - }); - - // 5. - // Update remaining storage entries - Self::update_ledger(&account, ledger); - ContractStake::::insert(&smart_contract, contract_stake_info); - - if new_staking_info.is_empty() { - StakerInfo::::remove(&account, &smart_contract); - } else { - StakerInfo::::insert(&account, &smart_contract, new_staking_info); - } - - Self::deposit_event(Event::::Unstake { - account, - smart_contract, - amount, - }); - - Ok(()) - } - - /// TODO - #[pallet::call_index(11)] - #[pallet::weight(Weight::zero())] - pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - let protocol_state = ActiveProtocolState::::get(); - let mut ledger = Ledger::::get(&account); - - // TODO: how do we handle expired rewards? Add an additional call to clean them up? - // Putting this logic inside existing calls will add even more complexity. - - // Check if the rewards have expired - let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; - ensure!( - staked_period - >= protocol_state - .period_number() - .saturating_sub(T::RewardRetentionInPeriods::get()), - Error::::StakerRewardsExpired - ); - - // Calculate the reward claim span - let (first_chunk, last_chunk) = ledger - .first_and_last_stake_chunks() - .ok_or(Error::::InternalClaimStakerError)?; - let era_rewards = - EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) - .ok_or(Error::::NoClaimableRewards)?; - - // The last era for which we can theoretically claim rewards. - // And indicator if we know the period's ending era. - let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { - (protocol_state.era.saturating_sub(1), None) - } else { - PeriodEnd::::get(&staked_period) - .map(|info| (info.final_era, Some(info.final_era))) - .ok_or(Error::::InternalClaimStakerError)? - }; - - // The last era for which we can claim rewards for this account. - // Limiting factors are: - // 1. era reward span entries - // 2. last era in period limit - // 3. last era in which staker had non-zero stake - let last_claim_era = if last_chunk.get_amount().is_zero() { - era_rewards - .last_era() - .min(last_period_era) - .min(last_chunk.get_era()) - } else { - era_rewards.last_era().min(last_period_era) - }; - - // Get chunks for reward claiming - let chunks_for_claim = - ledger - .claim_up_to_era(last_claim_era, period_end) - .map_err(|err| match err { - AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, - _ => Error::::InternalClaimStakerError, - })?; - - // Calculate rewards - let mut rewards: Vec<_> = Vec::new(); - let mut reward_sum = Balance::zero(); - for era in first_chunk.get_era()..=last_claim_era { - // TODO: this should be zipped, and values should be fetched only once - let era_reward = era_rewards - .get(era) - .ok_or(Error::::InternalClaimStakerError)?; - - let chunk = chunks_for_claim - .get(era) - .ok_or(Error::::InternalClaimStakerError)?; - - // Optimization, and zero-division protection - if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { - continue; - } - let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) - * era_reward.staker_reward_pool(); - - rewards.push((era, staker_reward)); - reward_sum.saturating_accrue(staker_reward); - } - - T::Currency::deposit_into_existing(&account, reward_sum) - .map_err(|_| Error::::InternalClaimStakerError)?; - - Self::update_ledger(&account, ledger); - - rewards.into_iter().for_each(|(era, reward)| { - Self::deposit_event(Event::::Reward { - account: account.clone(), - era, - amount: reward, - }); - }); - - Ok(()) - } - } - - impl Pallet { - /// `Err` if pallet disabled for maintenance, `Ok` otherwise. - pub(crate) fn ensure_pallet_enabled() -> Result<(), Error> { - if ActiveProtocolState::::get().maintenance { - Err(Error::::Disabled) - } else { - Ok(()) - } - } - - /// Ensure that the origin is either the `ManagerOrigin` or a signed origin. - /// - /// In case of manager, `Ok(None)` is returned, and if signed origin `Ok(Some(AccountId))` is returned. - pub(crate) fn ensure_signed_or_manager( - origin: T::RuntimeOrigin, - ) -> Result, BadOrigin> { - if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() { - return Ok(None); - } - let who = ensure_signed(origin)?; - Ok(Some(who)) - } - - /// Update the account ledger, and dApp staking balance lock. - /// - /// In case account ledger is empty, entries from the DB are removed and lock is released. - pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { - if ledger.is_empty() { - Ledger::::remove(&account); - T::Currency::remove_lock(STAKING_ID, account); - } else { - T::Currency::set_lock( - STAKING_ID, - account, - ledger.active_locked_amount(), - WithdrawReasons::all(), - ); - Ledger::::insert(account, ledger); - } - } - - /// Returns the number of blocks per voting period. - pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { - T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) - } - - /// `true` if smart contract is active, `false` if it has been unregistered. - fn is_active(smart_contract: &T::SmartContract) -> bool { - IntegratedDApps::::get(smart_contract) - .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) - } - - /// Calculates the `EraRewardSpan` index for the specified era. - pub fn era_reward_span_index(era: EraNumber) -> EraNumber { - era.saturating_sub(era % T::EraRewardSpanLength::get()) - } - } -} +// #[frame_support::pallet] +// pub mod pallet { +// use super::*; + +// /// The current storage version. +// const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + +// #[pallet::pallet] +// #[pallet::storage_version(STORAGE_VERSION)] +// pub struct Pallet(_); + +// #[pallet::config] +// pub trait Config: frame_system::Config { +// /// The overarching event type. +// type RuntimeEvent: From> + IsType<::RuntimeEvent>; + +// /// Currency used for staking. +// /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) +// type Currency: LockableCurrency< +// Self::AccountId, +// Moment = Self::BlockNumber, +// Balance = Balance, +// >; + +// /// Describes smart contract in the context required by dApp staking. +// type SmartContract: Parameter + Member + MaxEncodedLen; + +// /// Privileged origin for managing dApp staking pallet. +// type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; + +// /// Length of a standard era in block numbers. +// #[pallet::constant] +// type StandardEraLength: Get; + +// /// Length of the `Voting` period in standard eras. +// /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras +// /// for the sake of simplicity & consistency. +// #[pallet::constant] +// type StandardErasPerVotingPeriod: Get; + +// /// Length of the `Build&Earn` period in standard eras. +// /// Each `Build&Earn` period consists of one or more distinct standard eras. +// #[pallet::constant] +// type StandardErasPerBuildAndEarnPeriod: Get; + +// /// Maximum length of a single era reward span length entry. +// #[pallet::constant] +// type EraRewardSpanLength: Get; + +// /// Number of periods for which we keep rewards available for claiming. +// /// After that period, they are no longer claimable. +// #[pallet::constant] +// type RewardRetentionInPeriods: Get; + +// /// Maximum number of contracts that can be integrated into dApp staking at once. +// #[pallet::constant] +// type MaxNumberOfContracts: Get; + +// /// Maximum number of unlocking chunks that can exist per account at a time. +// #[pallet::constant] +// type MaxUnlockingChunks: Get; + +// /// Minimum amount an account has to lock in dApp staking in order to participate. +// #[pallet::constant] +// type MinimumLockedAmount: Get; + +// /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. +// #[pallet::constant] +// type UnlockingPeriod: Get>; + +// /// Maximum number of staking chunks that can exist per account at a time. +// #[pallet::constant] +// type MaxStakingChunks: Get; + +// /// Minimum amount staker can stake on a contract. +// #[pallet::constant] +// type MinimumStakeAmount: Get; +// } + +// #[pallet::event] +// #[pallet::generate_deposit(pub(crate) fn deposit_event)] +// pub enum Event { +// /// New era has started. +// NewEra { era: EraNumber }, +// /// New period has started. +// NewPeriod { +// period_type: PeriodType, +// number: PeriodNumber, +// }, +// /// A smart contract has been registered for dApp staking +// DAppRegistered { +// owner: T::AccountId, +// smart_contract: T::SmartContract, +// dapp_id: DAppId, +// }, +// /// dApp reward destination has been updated. +// DAppRewardDestinationUpdated { +// smart_contract: T::SmartContract, +// beneficiary: Option, +// }, +// /// dApp owner has been changed. +// DAppOwnerChanged { +// smart_contract: T::SmartContract, +// new_owner: T::AccountId, +// }, +// /// dApp has been unregistered +// DAppUnregistered { +// smart_contract: T::SmartContract, +// era: EraNumber, +// }, +// /// Account has locked some amount into dApp staking. +// Locked { +// account: T::AccountId, +// amount: Balance, +// }, +// /// Account has started the unlocking process for some amount. +// Unlocking { +// account: T::AccountId, +// amount: Balance, +// }, +// /// Account has claimed unlocked amount, removing the lock from it. +// ClaimedUnlocked { +// account: T::AccountId, +// amount: Balance, +// }, +// /// Account has relocked all of the unlocking chunks. +// Relock { +// account: T::AccountId, +// amount: Balance, +// }, +// /// Account has staked some amount on a smart contract. +// Stake { +// account: T::AccountId, +// smart_contract: T::SmartContract, +// amount: Balance, +// }, +// /// Account has unstaked some amount from a smart contract. +// Unstake { +// account: T::AccountId, +// smart_contract: T::SmartContract, +// amount: Balance, +// }, +// /// Account has claimed some stake rewards. +// Reward { +// account: T::AccountId, +// era: EraNumber, +// amount: Balance, +// }, +// } + +// #[pallet::error] +// pub enum Error { +// /// Pallet is disabled/in maintenance mode. +// Disabled, +// /// Smart contract already exists within dApp staking protocol. +// ContractAlreadyExists, +// /// Maximum number of smart contracts has been reached. +// ExceededMaxNumberOfContracts, +// /// Not possible to assign a new dApp Id. +// /// This should never happen since current type can support up to 65536 - 1 unique dApps. +// NewDAppIdUnavailable, +// /// Specified smart contract does not exist in dApp staking. +// ContractNotFound, +// /// Call origin is not dApp owner. +// OriginNotOwner, +// /// dApp is part of dApp staking but isn't active anymore. +// NotOperatedDApp, +// /// Performing locking or staking with 0 amount. +// ZeroAmount, +// /// Total locked amount for staker is below minimum threshold. +// LockedAmountBelowThreshold, +// /// Cannot add additional locked balance chunks due to capacity limit. +// TooManyLockedBalanceChunks, +// /// Cannot add additional unlocking chunks due to capacity limit. +// TooManyUnlockingChunks, +// /// Remaining stake prevents entire balance of starting the unlocking process. +// RemainingStakePreventsFullUnlock, +// /// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all. +// NoUnlockedChunksToClaim, +// /// There are no unlocking chunks available to relock. +// NoUnlockingChunks, +// /// The amount being staked is too large compared to what's available for staking. +// UnavailableStakeFunds, +// /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. +// UnclaimedRewardsFromPastPeriods, +// /// Cannot add additional stake chunks due to capacity limit. +// TooManyStakeChunks, +// /// An unexpected error occured while trying to stake. +// InternalStakeError, +// /// Total staked amount on contract is below the minimum required value. +// InsufficientStakeAmount, +// /// Stake operation is rejected since period ends in the next era. +// PeriodEndsInNextEra, +// /// Unstaking is rejected since the period in which past stake was active has passed. +// UnstakeFromPastPeriod, +// /// Unstake amount is greater than the staked amount. +// UnstakeAmountTooLarge, +// /// Account has no staking information for the contract. +// NoStakingInfo, +// /// An unexpected error occured while trying to unstake. +// InternalUnstakeError, +// /// Rewards are no longer claimable since they are too old. +// StakerRewardsExpired, +// /// There are no claimable rewards for the account. +// NoClaimableRewards, +// /// An unexpected error occured while trying to claim rewards. +// InternalClaimStakerError, +// } + +// /// General information about dApp staking protocol state. +// #[pallet::storage] +// pub type ActiveProtocolState = +// StorageValue<_, ProtocolState>, ValueQuery>; + +// /// Counter for unique dApp identifiers. +// #[pallet::storage] +// pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; + +// /// Map of all dApps integrated into dApp staking protocol. +// #[pallet::storage] +// pub type IntegratedDApps = CountedStorageMap< +// _, +// Blake2_128Concat, +// T::SmartContract, +// DAppInfo, +// OptionQuery, +// >; + +// /// General locked/staked information for each account. +// #[pallet::storage] +// pub type Ledger = +// StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; + +// /// Information about how much each staker has staked for each smart contract in some period. +// #[pallet::storage] +// pub type StakerInfo = StorageDoubleMap< +// _, +// Blake2_128Concat, +// T::AccountId, +// Blake2_128Concat, +// T::SmartContract, +// SingularStakingInfo, +// OptionQuery, +// >; + +// /// Information about how much has been staked on a smart contract in some era or period. +// #[pallet::storage] +// pub type ContractStake = +// StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakingInfoSeries, ValueQuery>; + +// /// General information about the current era. +// #[pallet::storage] +// pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; + +// /// Information about rewards for each era. +// /// +// /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. +// /// For the sake of simplicity, valid `era` keys are calculated as: +// /// +// /// era_key = era - (era % T::EraRewardSpanLength) +// /// +// /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. +// /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. +// #[pallet::storage] +// pub type EraRewards = +// StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan, OptionQuery>; + +// /// Information about period's end. +// #[pallet::storage] +// pub type PeriodEnd = +// StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; + +// #[pallet::hooks] +// impl Hooks> for Pallet { +// fn on_initialize(now: BlockNumberFor) -> Weight { +// let mut protocol_state = ActiveProtocolState::::get(); + +// // We should not modify pallet storage while in maintenance mode. +// // This is a safety measure, since maintenance mode is expected to be +// // enabled in case some misbehavior or corrupted storage is detected. +// if protocol_state.maintenance { +// return T::DbWeight::get().reads(1); +// } + +// // Nothing to do if it's not new era +// if !protocol_state.is_new_era(now) { +// return T::DbWeight::get().reads(1); +// } + +// let mut era_info = CurrentEraInfo::::get(); + +// let current_era = protocol_state.era; +// let next_era = current_era.saturating_add(1); +// let (maybe_period_event, era_reward) = match protocol_state.period_type() { +// PeriodType::Voting => { +// // For the sake of consistency, we put zero reward into storage +// let era_reward = +// EraReward::new(Balance::zero(), era_info.total_staked_amount()); + +// let ending_era = +// next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); +// let build_and_earn_start_block = +// now.saturating_add(T::StandardEraLength::get()); +// protocol_state.next_period_type(ending_era, build_and_earn_start_block); + +// era_info.migrate_to_next_era(Some(protocol_state.period_type())); + +// ( +// Some(Event::::NewPeriod { +// period_type: protocol_state.period_type(), +// number: protocol_state.period_number(), +// }), +// era_reward, +// ) +// } +// 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 era_reward = +// EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + +// // Switch to `Voting` period if conditions are met. +// if protocol_state.period_info.is_next_period(next_era) { +// // Store info about period end +// let bonus_reward_pool = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet +// PeriodEnd::::insert( +// &protocol_state.period_number(), +// PeriodEndInfo { +// bonus_reward_pool, +// total_vp_stake: era_info.staked_amount(PeriodType::Voting), +// final_era: current_era, +// }, +// ); + +// // For the sake of consistency we treat the whole `Voting` period as a single era. +// // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. +// let ending_era = next_era.saturating_add(1); +// let voting_period_length = Self::blocks_per_voting_period(); +// let next_era_start_block = now.saturating_add(voting_period_length); + +// protocol_state.next_period_type(ending_era, next_era_start_block); + +// era_info.migrate_to_next_era(Some(protocol_state.period_type())); + +// // TODO: trigger tier configuration calculation based on internal & external params. + +// ( +// Some(Event::::NewPeriod { +// period_type: protocol_state.period_type(), +// number: protocol_state.period_number(), +// }), +// era_reward, +// ) +// } else { +// let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); +// protocol_state.next_era_start = next_era_start_block; + +// era_info.migrate_to_next_era(None); + +// (None, era_reward) +// } +// } +// }; + +// // Update storage items + +// protocol_state.era = next_era; +// ActiveProtocolState::::put(protocol_state); + +// CurrentEraInfo::::put(era_info); + +// let era_span_index = Self::era_reward_span_index(current_era); +// let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); +// // TODO: error must not happen here. Log an error if it does. +// // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. +// // Will require a fix from the runtime team though. +// let _ = span.push(current_era, era_reward); +// EraRewards::::insert(&era_span_index, span); + +// Self::deposit_event(Event::::NewEra { era: next_era }); +// if let Some(period_event) = maybe_period_event { +// Self::deposit_event(period_event); +// } + +// // TODO: benchmark later +// T::DbWeight::get().reads_writes(3, 3) +// } +// } + +// #[pallet::call] +// impl Pallet { +// /// Used to enable or disable maintenance mode. +// /// Can only be called by manager origin. +// #[pallet::call_index(0)] +// #[pallet::weight(Weight::zero())] +// pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { +// T::ManagerOrigin::ensure_origin(origin)?; +// ActiveProtocolState::::mutate(|state| state.maintenance = enabled); +// Ok(()) +// } + +// /// Used to register a new contract for dApp staking. +// /// +// /// If successful, smart contract will be assigned a simple, unique numerical identifier. +// #[pallet::call_index(1)] +// #[pallet::weight(Weight::zero())] +// pub fn register( +// origin: OriginFor, +// owner: T::AccountId, +// smart_contract: T::SmartContract, +// ) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// T::ManagerOrigin::ensure_origin(origin)?; + +// ensure!( +// !IntegratedDApps::::contains_key(&smart_contract), +// Error::::ContractAlreadyExists, +// ); + +// ensure!( +// IntegratedDApps::::count() < T::MaxNumberOfContracts::get().into(), +// Error::::ExceededMaxNumberOfContracts +// ); + +// let dapp_id = NextDAppId::::get(); +// // MAX value must never be assigned as a dApp Id since it serves as a sentinel value. +// ensure!(dapp_id < DAppId::MAX, Error::::NewDAppIdUnavailable); + +// IntegratedDApps::::insert( +// &smart_contract, +// DAppInfo { +// owner: owner.clone(), +// id: dapp_id, +// state: DAppState::Registered, +// reward_destination: None, +// }, +// ); + +// NextDAppId::::put(dapp_id.saturating_add(1)); + +// Self::deposit_event(Event::::DAppRegistered { +// owner, +// smart_contract, +// dapp_id, +// }); + +// Ok(()) +// } + +// /// Used to modify the reward destination account for a dApp. +// /// +// /// Caller has to be dApp owner. +// /// If set to `None`, rewards will be deposited to the dApp owner. +// #[pallet::call_index(2)] +// #[pallet::weight(Weight::zero())] +// pub fn set_dapp_reward_destination( +// origin: OriginFor, +// smart_contract: T::SmartContract, +// beneficiary: Option, +// ) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let dev_account = ensure_signed(origin)?; + +// IntegratedDApps::::try_mutate( +// &smart_contract, +// |maybe_dapp_info| -> DispatchResult { +// let dapp_info = maybe_dapp_info +// .as_mut() +// .ok_or(Error::::ContractNotFound)?; + +// ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner); + +// dapp_info.reward_destination = beneficiary.clone(); + +// Ok(()) +// }, +// )?; + +// Self::deposit_event(Event::::DAppRewardDestinationUpdated { +// smart_contract, +// beneficiary, +// }); + +// Ok(()) +// } + +// /// Used to change dApp owner. +// /// +// /// Can be called by dApp owner or dApp staking manager origin. +// /// This is useful in two cases: +// /// 1. when the dApp owner account is compromised, manager can change the owner to a new account +// /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). +// #[pallet::call_index(3)] +// #[pallet::weight(Weight::zero())] +// pub fn set_dapp_owner( +// origin: OriginFor, +// smart_contract: T::SmartContract, +// new_owner: T::AccountId, +// ) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let origin = Self::ensure_signed_or_manager(origin)?; + +// IntegratedDApps::::try_mutate( +// &smart_contract, +// |maybe_dapp_info| -> DispatchResult { +// let dapp_info = maybe_dapp_info +// .as_mut() +// .ok_or(Error::::ContractNotFound)?; + +// // If manager origin, `None`, no need to check if caller is the owner. +// if let Some(caller) = origin { +// ensure!(dapp_info.owner == caller, Error::::OriginNotOwner); +// } + +// dapp_info.owner = new_owner.clone(); + +// Ok(()) +// }, +// )?; + +// Self::deposit_event(Event::::DAppOwnerChanged { +// smart_contract, +// new_owner, +// }); + +// Ok(()) +// } + +// /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. +// /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. +// /// +// /// Can be called by dApp owner or dApp staking manager origin. +// #[pallet::call_index(4)] +// #[pallet::weight(Weight::zero())] +// pub fn unregister( +// origin: OriginFor, +// smart_contract: T::SmartContract, +// ) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// T::ManagerOrigin::ensure_origin(origin)?; + +// let current_era = ActiveProtocolState::::get().era; + +// IntegratedDApps::::try_mutate( +// &smart_contract, +// |maybe_dapp_info| -> DispatchResult { +// let dapp_info = maybe_dapp_info +// .as_mut() +// .ok_or(Error::::ContractNotFound)?; + +// ensure!( +// dapp_info.state == DAppState::Registered, +// Error::::NotOperatedDApp +// ); + +// dapp_info.state = DAppState::Unregistered(current_era); + +// Ok(()) +// }, +// )?; + +// // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. + +// // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' + +// // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' + +// Self::deposit_event(Event::::DAppUnregistered { +// smart_contract, +// era: current_era, +// }); + +// Ok(()) +// } + +// /// Locks additional funds into dApp staking. +// /// +// /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. +// /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. +// /// +// /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, +// /// caller should claim pending rewards, before retrying to lock additional funds. +// #[pallet::call_index(5)] +// #[pallet::weight(Weight::zero())] +// pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// let mut ledger = Ledger::::get(&account); + +// // Calculate & check amount available for locking +// let available_balance = +// T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); +// let amount_to_lock = available_balance.min(amount); +// ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); + +// ledger.add_lock_amount(amount_to_lock); + +// ensure!( +// ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), +// Error::::LockedAmountBelowThreshold +// ); + +// Self::update_ledger(&account, ledger); +// CurrentEraInfo::::mutate(|era_info| { +// era_info.add_locked(amount_to_lock); +// }); + +// Self::deposit_event(Event::::Locked { +// account, +// amount: amount_to_lock, +// }); + +// Ok(()) +// } + +// /// Attempts to start the unlocking process for the specified amount. +// /// +// /// Only the amount that isn't actively used for staking can be unlocked. +// /// If the amount is greater than the available amount for unlocking, everything is unlocked. +// /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. +// #[pallet::call_index(6)] +// #[pallet::weight(Weight::zero())] +// pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// let state = ActiveProtocolState::::get(); +// let mut ledger = Ledger::::get(&account); + +// let available_for_unlocking = ledger.unlockable_amount(state.period_info.number); +// let amount_to_unlock = available_for_unlocking.min(amount); + +// // Ensure we unlock everything if remaining amount is below threshold. +// let remaining_amount = ledger +// .active_locked_amount() +// .saturating_sub(amount_to_unlock); +// let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { +// ensure!( +// ledger.active_stake(state.period_info.number).is_zero(), +// Error::::RemainingStakePreventsFullUnlock +// ); +// ledger.active_locked_amount() +// } else { +// amount_to_unlock +// }; + +// // Sanity check +// ensure!(!amount_to_unlock.is_zero(), Error::::ZeroAmount); + +// // Update ledger with new lock and unlocking amounts +// ledger.subtract_lock_amount(amount_to_unlock); + +// let current_block = frame_system::Pallet::::block_number(); +// let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); +// ledger +// .add_unlocking_chunk(amount_to_unlock, unlock_block) +// .map_err(|_| Error::::TooManyUnlockingChunks)?; + +// // Update storage +// Self::update_ledger(&account, ledger); +// CurrentEraInfo::::mutate(|era_info| { +// era_info.unlocking_started(amount_to_unlock); +// }); + +// Self::deposit_event(Event::::Unlocking { +// account, +// amount: amount_to_unlock, +// }); + +// Ok(()) +// } + +// /// Claims all of fully unlocked chunks, removing the lock from them. +// #[pallet::call_index(7)] +// #[pallet::weight(Weight::zero())] +// pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// let mut ledger = Ledger::::get(&account); + +// let current_block = frame_system::Pallet::::block_number(); +// let amount = ledger.claim_unlocked(current_block); +// ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); + +// Self::update_ledger(&account, ledger); +// CurrentEraInfo::::mutate(|era_info| { +// era_info.unlocking_removed(amount); +// }); + +// // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) + +// // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have + +// Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); + +// Ok(()) +// } + +// #[pallet::call_index(8)] +// #[pallet::weight(Weight::zero())] +// pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// let mut ledger = Ledger::::get(&account); + +// ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); + +// let amount = ledger.consume_unlocking_chunks(); + +// ledger.add_lock_amount(amount); +// ensure!( +// ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), +// Error::::LockedAmountBelowThreshold +// ); + +// Self::update_ledger(&account, ledger); +// CurrentEraInfo::::mutate(|era_info| { +// era_info.add_locked(amount); +// era_info.unlocking_removed(amount); +// }); + +// Self::deposit_event(Event::::Relock { account, amount }); + +// Ok(()) +// } + +// /// Stake the specified amount on a smart contract. +// /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. +// /// +// /// Depending on the period type, appropriate stake amount will be updated. +// #[pallet::call_index(9)] +// #[pallet::weight(Weight::zero())] +// pub fn stake( +// origin: OriginFor, +// smart_contract: T::SmartContract, +// #[pallet::compact] amount: Balance, +// ) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// ensure!(amount > 0, Error::::ZeroAmount); + +// ensure!( +// Self::is_active(&smart_contract), +// Error::::NotOperatedDApp +// ); + +// let protocol_state = ActiveProtocolState::::get(); +// // Staker always stakes from the NEXT era +// let stake_era = protocol_state.era.saturating_add(1); +// ensure!( +// !protocol_state.period_info.is_next_period(stake_era), +// Error::::PeriodEndsInNextEra +// ); + +// let mut ledger = Ledger::::get(&account); + +// // 1. +// // Increase stake amount for the next era & current period in staker's ledger +// ledger +// .add_stake_amount(amount, stake_era, protocol_state.period_number()) +// .map_err(|err| match err { +// AccountLedgerError::InvalidPeriod => { +// Error::::UnclaimedRewardsFromPastPeriods +// } +// AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, +// AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, +// // Defensive check, should never happen +// _ => Error::::InternalStakeError, +// })?; + +// // 2. +// // Update `StakerInfo` storage with the new stake amount on the specified contract. +// // +// // There are two distinct scenarios: +// // 1. Existing entry matches the current period number - just update it. +// // 2. Entry doesn't exist or it's for an older period - create a new one. +// // +// // This is ok since we only use this storage entry to keep track of how much each staker +// // has staked on each contract in the current period. We only ever need the latest information. +// // This is because `AccountLedger` is the one keeping information about how much was staked when. +// let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { +// Some(mut staking_info) +// if staking_info.period_number() == protocol_state.period_number() => +// { +// staking_info.stake(amount, protocol_state.period_info.period_type); +// staking_info +// } +// _ => { +// ensure!( +// amount >= T::MinimumStakeAmount::get(), +// Error::::InsufficientStakeAmount +// ); +// let mut staking_info = SingularStakingInfo::new( +// protocol_state.period_info.number, +// protocol_state.period_info.period_type, +// ); +// staking_info.stake(amount, protocol_state.period_info.period_type); +// staking_info +// } +// }; + +// // 3. +// // Update `ContractStake` storage with the new stake amount on the specified contract. +// let mut contract_stake_info = ContractStake::::get(&smart_contract); +// ensure!( +// contract_stake_info +// .stake(amount, protocol_state.period_info, stake_era) +// .is_ok(), +// Error::::InternalStakeError +// ); + +// // 4. +// // Update total staked amount for the next era. +// CurrentEraInfo::::mutate(|era_info| { +// era_info.add_stake_amount(amount, protocol_state.period_type()); +// }); + +// // 5. +// // Update remaining storage entries +// Self::update_ledger(&account, ledger); +// StakerInfo::::insert(&account, &smart_contract, new_staking_info); +// ContractStake::::insert(&smart_contract, contract_stake_info); + +// Self::deposit_event(Event::::Stake { +// account, +// smart_contract, +// amount, +// }); + +// Ok(()) +// } + +// /// Unstake the specified amount from a smart contract. +// /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. +// /// +// /// Depending on the period type, appropriate stake amount will be updated. +// #[pallet::call_index(10)] +// #[pallet::weight(Weight::zero())] +// pub fn unstake( +// origin: OriginFor, +// smart_contract: T::SmartContract, +// #[pallet::compact] amount: Balance, +// ) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// ensure!(amount > 0, Error::::ZeroAmount); + +// ensure!( +// Self::is_active(&smart_contract), +// Error::::NotOperatedDApp +// ); + +// let protocol_state = ActiveProtocolState::::get(); +// let unstake_era = protocol_state.era; + +// let mut ledger = Ledger::::get(&account); + +// // 1. +// // Update `StakerInfo` storage with the reduced stake amount on the specified contract. +// let (new_staking_info, amount) = match StakerInfo::::get(&account, &smart_contract) { +// Some(mut staking_info) => { +// ensure!( +// staking_info.period_number() == protocol_state.period_number(), +// Error::::UnstakeFromPastPeriod +// ); +// ensure!( +// staking_info.total_staked_amount() >= amount, +// Error::::UnstakeAmountTooLarge +// ); + +// // If unstaking would take the total staked amount below the minimum required value, +// // unstake everything. +// let amount = if staking_info.total_staked_amount().saturating_sub(amount) +// < T::MinimumStakeAmount::get() +// { +// staking_info.total_staked_amount() +// } else { +// amount +// }; + +// staking_info.unstake(amount, protocol_state.period_type()); +// (staking_info, amount) +// } +// None => { +// return Err(Error::::NoStakingInfo.into()); +// } +// }; + +// // 2. +// // Reduce stake amount +// ledger +// .unstake_amount(amount, unstake_era, protocol_state.period_number()) +// .map_err(|err| match err { +// AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, +// AccountLedgerError::UnstakeAmountLargerThanStake => { +// Error::::UnstakeAmountTooLarge +// } +// AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, +// _ => Error::::InternalUnstakeError, +// })?; + +// // 3. +// // Update `ContractStake` storage with the reduced stake amount on the specified contract. +// let mut contract_stake_info = ContractStake::::get(&smart_contract); +// ensure!( +// contract_stake_info +// .unstake(amount, protocol_state.period_info, unstake_era) +// .is_ok(), +// Error::::InternalUnstakeError +// ); + +// // 4. +// // Update total staked amount for the next era. +// CurrentEraInfo::::mutate(|era_info| { +// era_info.unstake_amount(amount, protocol_state.period_type()); +// }); + +// // 5. +// // Update remaining storage entries +// Self::update_ledger(&account, ledger); +// ContractStake::::insert(&smart_contract, contract_stake_info); + +// if new_staking_info.is_empty() { +// StakerInfo::::remove(&account, &smart_contract); +// } else { +// StakerInfo::::insert(&account, &smart_contract, new_staking_info); +// } + +// Self::deposit_event(Event::::Unstake { +// account, +// smart_contract, +// amount, +// }); + +// Ok(()) +// } + +// /// TODO +// #[pallet::call_index(11)] +// #[pallet::weight(Weight::zero())] +// pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// let protocol_state = ActiveProtocolState::::get(); +// let mut ledger = Ledger::::get(&account); + +// // TODO: how do we handle expired rewards? Add an additional call to clean them up? +// // Putting this logic inside existing calls will add even more complexity. + +// // Check if the rewards have expired +// let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; +// ensure!( +// staked_period +// >= protocol_state +// .period_number() +// .saturating_sub(T::RewardRetentionInPeriods::get()), +// Error::::StakerRewardsExpired +// ); + +// // Calculate the reward claim span +// let (first_chunk, last_chunk) = ledger +// .first_and_last_stake_chunks() +// .ok_or(Error::::InternalClaimStakerError)?; +// let era_rewards = +// EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) +// .ok_or(Error::::NoClaimableRewards)?; + +// // The last era for which we can theoretically claim rewards. +// // And indicator if we know the period's ending era. +// let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { +// (protocol_state.era.saturating_sub(1), None) +// } else { +// PeriodEnd::::get(&staked_period) +// .map(|info| (info.final_era, Some(info.final_era))) +// .ok_or(Error::::InternalClaimStakerError)? +// }; + +// // The last era for which we can claim rewards for this account. +// // Limiting factors are: +// // 1. era reward span entries +// // 2. last era in period limit +// // 3. last era in which staker had non-zero stake +// let last_claim_era = if last_chunk.get_amount().is_zero() { +// era_rewards +// .last_era() +// .min(last_period_era) +// .min(last_chunk.get_era()) +// } else { +// era_rewards.last_era().min(last_period_era) +// }; + +// // Get chunks for reward claiming +// let chunks_for_claim = +// ledger +// .claim_up_to_era(last_claim_era, period_end) +// .map_err(|err| match err { +// AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, +// _ => Error::::InternalClaimStakerError, +// })?; + +// // Calculate rewards +// let mut rewards: Vec<_> = Vec::new(); +// let mut reward_sum = Balance::zero(); +// for era in first_chunk.get_era()..=last_claim_era { +// // TODO: this should be zipped, and values should be fetched only once +// let era_reward = era_rewards +// .get(era) +// .ok_or(Error::::InternalClaimStakerError)?; + +// let chunk = chunks_for_claim +// .get(era) +// .ok_or(Error::::InternalClaimStakerError)?; + +// // Optimization, and zero-division protection +// if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { +// continue; +// } +// let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) +// * era_reward.staker_reward_pool(); + +// rewards.push((era, staker_reward)); +// reward_sum.saturating_accrue(staker_reward); +// } + +// T::Currency::deposit_into_existing(&account, reward_sum) +// .map_err(|_| Error::::InternalClaimStakerError)?; + +// Self::update_ledger(&account, ledger); + +// rewards.into_iter().for_each(|(era, reward)| { +// Self::deposit_event(Event::::Reward { +// account: account.clone(), +// era, +// amount: reward, +// }); +// }); + +// Ok(()) +// } +// } + +// impl Pallet { +// /// `Err` if pallet disabled for maintenance, `Ok` otherwise. +// pub(crate) fn ensure_pallet_enabled() -> Result<(), Error> { +// if ActiveProtocolState::::get().maintenance { +// Err(Error::::Disabled) +// } else { +// Ok(()) +// } +// } + +// /// Ensure that the origin is either the `ManagerOrigin` or a signed origin. +// /// +// /// In case of manager, `Ok(None)` is returned, and if signed origin `Ok(Some(AccountId))` is returned. +// pub(crate) fn ensure_signed_or_manager( +// origin: T::RuntimeOrigin, +// ) -> Result, BadOrigin> { +// if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() { +// return Ok(None); +// } +// let who = ensure_signed(origin)?; +// Ok(Some(who)) +// } + +// /// Update the account ledger, and dApp staking balance lock. +// /// +// /// In case account ledger is empty, entries from the DB are removed and lock is released. +// pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { +// if ledger.is_empty() { +// Ledger::::remove(&account); +// T::Currency::remove_lock(STAKING_ID, account); +// } else { +// T::Currency::set_lock( +// STAKING_ID, +// account, +// ledger.active_locked_amount(), +// WithdrawReasons::all(), +// ); +// Ledger::::insert(account, ledger); +// } +// } + +// /// Returns the number of blocks per voting period. +// pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { +// T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) +// } + +// /// `true` if smart contract is active, `false` if it has been unregistered. +// fn is_active(smart_contract: &T::SmartContract) -> bool { +// IntegratedDApps::::get(smart_contract) +// .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) +// } + +// /// Calculates the `EraRewardSpan` index for the specified era. +// pub fn era_reward_span_index(era: EraNumber) -> EraNumber { +// era.saturating_sub(era % T::EraRewardSpanLength::get()) +// } +// } +// } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 1cd94100b3..c3b15bdc1c 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -39,215 +39,215 @@ pub(crate) type Balance = u128; pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; pub(crate) const MINIMUM_LOCK_AMOUNT: Balance = 10; -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlock; - -construct_runtime!( - pub struct Test - where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system, - Balances: pallet_balances, - DappStaking: pallet_dapp_staking, - } -); - -parameter_types! { - pub const BlockHashCount: u64 = 250; - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); -} - -impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type RuntimeCall = RuntimeCall; - type BlockNumber = BlockNumber; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = AccountId; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type DbWeight = (); - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; -} - -impl pallet_balances::Config for Test { - type MaxLocks = ConstU32<4>; - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - type Balance = Balance; - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ConstU128; - type AccountStore = System; - type HoldIdentifier = (); - type FreezeIdentifier = (); - type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; - type WeightInfo = (); -} - -impl pallet_dapp_staking::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type SmartContract = MockSmartContract; - type ManagerOrigin = frame_system::EnsureRoot; - type StandardEraLength = ConstU64<10>; - type StandardErasPerVotingPeriod = ConstU32<8>; - type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; - type EraRewardSpanLength = ConstU32<8>; - type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<10>; - type MaxUnlockingChunks = ConstU32<5>; - type MaxStakingChunks = ConstU32<8>; - type MinimumLockedAmount = ConstU128; - type UnlockingPeriod = ConstU64<20>; - type MinimumStakeAmount = ConstU128<3>; -} - -// TODO: why not just change this to e.g. u32 for test? -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -pub enum MockSmartContract { - Wasm(AccountId), - Other(AccountId), -} - -impl Default for MockSmartContract { - fn default() -> Self { - MockSmartContract::Wasm(1) - } -} - -pub struct ExtBuilder; -impl ExtBuilder { - pub fn build() -> TestExternalities { - let mut storage = frame_system::GenesisConfig::default() - .build_storage::() - .unwrap(); - - let balances = vec![1000; 9] - .into_iter() - .enumerate() - .map(|(idx, amount)| (idx as u64 + 1, amount)) - .collect(); - - pallet_balances::GenesisConfig:: { balances: balances } - .assimilate_storage(&mut storage) - .ok(); - - let mut ext = TestExternalities::from(storage); - ext.execute_with(|| { - System::set_block_number(1); - - // TODO: not sure why the mess with type happens here, I can check it later - let era_length: BlockNumber = - <::StandardEraLength as sp_core::Get<_>>::get(); - let voting_period_length_in_eras: EraNumber = - <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( - ); - - // TODO: handle this via GenesisConfig, and some helper functions to set the state - pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { - era: 1, - next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, - period_info: PeriodInfo { - number: 1, - period_type: PeriodType::Voting, - ending_era: 2, - }, - maintenance: false, - }); - - // DappStaking::on_initialize(System::block_number()); - }); - - ext - } -} - -/// Run to the specified block number. -/// Function assumes first block has been initialized. -pub(crate) fn run_to_block(n: u64) { - while System::block_number() < n { - DappStaking::on_finalize(System::block_number()); - System::set_block_number(System::block_number() + 1); - // This is performed outside of dapps staking but we expect it before on_initialize - DappStaking::on_initialize(System::block_number()); - } -} - -/// Run for the specified number of blocks. -/// Function assumes first block has been initialized. -pub(crate) fn run_for_blocks(n: u64) { - run_to_block(System::block_number() + n); -} - -/// Advance blocks until the specified era has been reached. -/// -/// Function has no effect if era is already passed. -pub(crate) fn advance_to_era(era: EraNumber) { - assert!(era >= ActiveProtocolState::::get().era); - while ActiveProtocolState::::get().era < era { - run_for_blocks(1); - } -} - -/// Advance blocks until next era has been reached. -pub(crate) fn advance_to_next_era() { - advance_to_era(ActiveProtocolState::::get().era + 1); -} - -/// Advance blocks until the specified period has been reached. -/// -/// Function has no effect if period is already passed. -pub(crate) fn advance_to_period(period: PeriodNumber) { - assert!(period >= ActiveProtocolState::::get().period_number()); - while ActiveProtocolState::::get().period_number() < period { - run_for_blocks(1); - } -} - -/// Advance blocks until next period has been reached. -pub(crate) fn advance_to_next_period() { - advance_to_period(ActiveProtocolState::::get().period_number() + 1); -} - -/// Advance blocks until next period type has been reached. -pub(crate) fn _advance_to_next_period_type() { - let period_type = ActiveProtocolState::::get().period_type(); - while ActiveProtocolState::::get().period_type() == period_type { - run_for_blocks(1); - } -} - -// Return all dApp staking events from the event buffer. -pub fn dapp_staking_events() -> Vec> { - System::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let RuntimeEvent::DappStaking(inner) = e { - Some(inner) - } else { - None - } - }) - .collect() -} +// type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +// type Block = frame_system::mocking::MockBlock; + +// construct_runtime!( +// pub struct Test +// where +// Block = Block, +// NodeBlock = Block, +// UncheckedExtrinsic = UncheckedExtrinsic, +// { +// System: frame_system, +// Balances: pallet_balances, +// DappStaking: pallet_dapp_staking, +// } +// ); + +// parameter_types! { +// pub const BlockHashCount: u64 = 250; +// pub BlockWeights: frame_system::limits::BlockWeights = +// frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +// } + +// impl frame_system::Config for Test { +// type BaseCallFilter = frame_support::traits::Everything; +// type BlockWeights = (); +// type BlockLength = (); +// type RuntimeOrigin = RuntimeOrigin; +// type Index = u64; +// type RuntimeCall = RuntimeCall; +// type BlockNumber = BlockNumber; +// type Hash = H256; +// type Hashing = BlakeTwo256; +// type AccountId = AccountId; +// type Lookup = IdentityLookup; +// type Header = Header; +// type RuntimeEvent = RuntimeEvent; +// type BlockHashCount = BlockHashCount; +// type DbWeight = (); +// type Version = (); +// type PalletInfo = PalletInfo; +// type AccountData = pallet_balances::AccountData; +// type OnNewAccount = (); +// type OnKilledAccount = (); +// type SystemWeightInfo = (); +// type SS58Prefix = (); +// type OnSetCode = (); +// type MaxConsumers = frame_support::traits::ConstU32<16>; +// } + +// impl pallet_balances::Config for Test { +// type MaxLocks = ConstU32<4>; +// type MaxReserves = (); +// type ReserveIdentifier = [u8; 8]; +// type Balance = Balance; +// type RuntimeEvent = RuntimeEvent; +// type DustRemoval = (); +// type ExistentialDeposit = ConstU128; +// type AccountStore = System; +// type HoldIdentifier = (); +// type FreezeIdentifier = (); +// type MaxHolds = ConstU32<0>; +// type MaxFreezes = ConstU32<0>; +// type WeightInfo = (); +// } + +// impl pallet_dapp_staking::Config for Test { +// type RuntimeEvent = RuntimeEvent; +// type Currency = Balances; +// type SmartContract = MockSmartContract; +// type ManagerOrigin = frame_system::EnsureRoot; +// type StandardEraLength = ConstU64<10>; +// type StandardErasPerVotingPeriod = ConstU32<8>; +// type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; +// type EraRewardSpanLength = ConstU32<8>; +// type RewardRetentionInPeriods = ConstU32<2>; +// type MaxNumberOfContracts = ConstU16<10>; +// type MaxUnlockingChunks = ConstU32<5>; +// type MaxStakingChunks = ConstU32<8>; +// type MinimumLockedAmount = ConstU128; +// type UnlockingPeriod = ConstU64<20>; +// type MinimumStakeAmount = ConstU128<3>; +// } + +// // TODO: why not just change this to e.g. u32 for test? +// #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] +// pub enum MockSmartContract { +// Wasm(AccountId), +// Other(AccountId), +// } + +// impl Default for MockSmartContract { +// fn default() -> Self { +// MockSmartContract::Wasm(1) +// } +// } + +// pub struct ExtBuilder; +// impl ExtBuilder { +// pub fn build() -> TestExternalities { +// let mut storage = frame_system::GenesisConfig::default() +// .build_storage::() +// .unwrap(); + +// let balances = vec![1000; 9] +// .into_iter() +// .enumerate() +// .map(|(idx, amount)| (idx as u64 + 1, amount)) +// .collect(); + +// pallet_balances::GenesisConfig:: { balances: balances } +// .assimilate_storage(&mut storage) +// .ok(); + +// let mut ext = TestExternalities::from(storage); +// ext.execute_with(|| { +// System::set_block_number(1); + +// // TODO: not sure why the mess with type happens here, I can check it later +// let era_length: BlockNumber = +// <::StandardEraLength as sp_core::Get<_>>::get(); +// let voting_period_length_in_eras: EraNumber = +// <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( +// ); + +// // TODO: handle this via GenesisConfig, and some helper functions to set the state +// pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { +// era: 1, +// next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, +// period_info: PeriodInfo { +// number: 1, +// period_type: PeriodType::Voting, +// ending_era: 2, +// }, +// maintenance: false, +// }); + +// // DappStaking::on_initialize(System::block_number()); +// }); + +// ext +// } +// } + +// /// Run to the specified block number. +// /// Function assumes first block has been initialized. +// pub(crate) fn run_to_block(n: u64) { +// while System::block_number() < n { +// DappStaking::on_finalize(System::block_number()); +// System::set_block_number(System::block_number() + 1); +// // This is performed outside of dapps staking but we expect it before on_initialize +// DappStaking::on_initialize(System::block_number()); +// } +// } + +// /// Run for the specified number of blocks. +// /// Function assumes first block has been initialized. +// pub(crate) fn run_for_blocks(n: u64) { +// run_to_block(System::block_number() + n); +// } + +// /// Advance blocks until the specified era has been reached. +// /// +// /// Function has no effect if era is already passed. +// pub(crate) fn advance_to_era(era: EraNumber) { +// assert!(era >= ActiveProtocolState::::get().era); +// while ActiveProtocolState::::get().era < era { +// run_for_blocks(1); +// } +// } + +// /// Advance blocks until next era has been reached. +// pub(crate) fn advance_to_next_era() { +// advance_to_era(ActiveProtocolState::::get().era + 1); +// } + +// /// Advance blocks until the specified period has been reached. +// /// +// /// Function has no effect if period is already passed. +// pub(crate) fn advance_to_period(period: PeriodNumber) { +// assert!(period >= ActiveProtocolState::::get().period_number()); +// while ActiveProtocolState::::get().period_number() < period { +// run_for_blocks(1); +// } +// } + +// /// Advance blocks until next period has been reached. +// pub(crate) fn advance_to_next_period() { +// advance_to_period(ActiveProtocolState::::get().period_number() + 1); +// } + +// /// Advance blocks until next period type has been reached. +// pub(crate) fn _advance_to_next_period_type() { +// let period_type = ActiveProtocolState::::get().period_type(); +// while ActiveProtocolState::::get().period_type() == period_type { +// run_for_blocks(1); +// } +// } + +// // Return all dApp staking events from the event buffer. +// pub fn dapp_staking_events() -> Vec> { +// System::events() +// .into_iter() +// .map(|r| r.event) +// .filter_map(|e| { +// if let RuntimeEvent::DappStaking(inner) = e { +// Some(inner) +// } else { +// None +// } +// }) +// .collect() +// } diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs index 94a090243c..d4f5974862 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -17,6 +17,6 @@ // along with Astar. If not, see . mod mock; -mod testing_utils; -mod tests; +// mod testing_utils; +// mod tests; mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index dd301b983d..a9e6e336fe 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -34,414 +34,6 @@ macro_rules! get_u32_type { }; } -#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, Default)] -struct DummyEraAmount { - amount: Balance, - era: u32, -} -impl AmountEraPair for DummyEraAmount { - fn new(amount: Balance, era: u32) -> Self { - Self { amount, era } - } - fn get_amount(&self) -> Balance { - self.amount - } - fn get_era(&self) -> u32 { - self.era - } - fn set_era(&mut self, era: u32) { - self.era = era; - } - fn saturating_accrue(&mut self, increase: Balance) { - self.amount.saturating_accrue(increase); - } - fn saturating_reduce(&mut self, reduction: Balance) { - self.amount.saturating_reduce(reduction); - } -} -impl DummyEraAmount { - pub fn new(amount: Balance, era: u32) -> Self { - Self { amount, era } - } -} - -#[test] -fn sparse_bounded_amount_era_vec_add_amount_works() { - get_u32_type!(MaxLen, 5); - - // Sanity check - let mut vec = SparseBoundedAmountEraVec::::new(); - assert!(vec.0.is_empty()); - assert_ok!(vec.add_amount(0, 0)); - assert!(vec.0.is_empty()); - - // 1st scenario - add to empty vector, should create one entry - let init_amount = 19; - let first_era = 3; - assert_ok!(vec.add_amount(init_amount, first_era)); - assert_eq!(vec.0.len(), 1); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount, first_era)); - - // 2nd scenario - add to the same era, should update the entry - assert_ok!(vec.add_amount(init_amount, first_era)); - assert_eq!(vec.0.len(), 1); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); - - // 3rd scenario - add to the next era, should create a new entry - let second_era = first_era + 1; - assert_ok!(vec.add_amount(init_amount, second_era)); - assert_eq!(vec.0.len(), 2); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); - assert_eq!(vec.0[1], DummyEraAmount::new(init_amount * 3, second_era)); - - // 4th scenario - add to the previous era, should fail and be a noop - assert_eq!( - vec.add_amount(init_amount, first_era), - Err(AccountLedgerError::OldEra) - ); - assert_eq!(vec.0.len(), 2); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); - assert_eq!(vec.0[1], DummyEraAmount::new(init_amount * 3, second_era)); - - // 5th scenario - exceed capacity, should fail - for i in vec.0.len()..MaxLen::get() as usize { - assert_ok!(vec.add_amount(init_amount, second_era + i as u32)); - } - assert_eq!( - vec.add_amount(init_amount, 100), - Err(AccountLedgerError::NoCapacity) - ); -} - -// Test two scenarios: -// -// 1. [amount, era] -> subtract(x, era) -> [amount - x, era] -// 2. [amount, era] -> subtract (amount * 2, era) -> [] -#[test] -fn sparse_bounded_amount_era_vec_subtract_amount_basic_scenario_works() { - get_u32_type!(MaxLen, 5); - - // Sanity check - let mut vec = SparseBoundedAmountEraVec::::new(); - assert_ok!(vec.subtract_amount(0, 0)); - assert!(vec.0.is_empty()); - - // 1st scenario - only one entry exists, and it's the same era as the unlock - let init_amount = 19; - let first_era = 1; - let sub_amount = 3; - assert_ok!(vec.add_amount(init_amount, first_era)); - assert_ok!(vec.subtract_amount(sub_amount, first_era)); - assert_eq!(vec.0.len(), 1); - assert_eq!( - vec.0[0], - DummyEraAmount::new(init_amount - sub_amount, first_era), - "Only single entry and it should be updated." - ); - - // 2nd scenario - subtract everything (and more - underflow!) from the current era, causing full removal. Should cleanup the vector. - assert_ok!(vec.subtract_amount(init_amount * 2, first_era)); - assert!(vec.0.is_empty(), "Full removal should cleanup the vector."); -} - -#[test] -fn sparse_bounded_amount_era_vec_subtract_amount_advanced_consecutive_works() { - get_u32_type!(MaxLen, 5); - let mut vec = SparseBoundedAmountEraVec::::new(); - - // 1st scenario - two entries, consecutive eras, subtract from the second era. - // Only the second entry should be updated. - let (first_era, second_era) = (1, 2); - let (first_amount, second_amount) = (19, 23); - assert_ok!(vec.add_amount(first_amount, first_era)); - assert_ok!(vec.add_amount(second_amount, second_era)); - - let sub_amount = 3; - assert_ok!(vec.subtract_amount(sub_amount, second_era)); - assert_eq!(vec.0.len(), 2); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "First entry should remain unchanged." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount + second_amount - sub_amount, second_era), - "Second entry should have it's amount reduced by the subtracted amount." - ); - - // 2nd scenario - two entries, consecutive eras, subtract from the first era. - // Both the first and second entry should be updated. - assert_ok!(vec.subtract_amount(sub_amount, first_era)); - assert_eq!(vec.0.len(), 2); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount - sub_amount, first_era), - "First entry is updated since it was specified." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount + second_amount - sub_amount * 2, second_era), - "Second entry is updated because it comes AFTER the first one - same applies to all future entries." - ); - - // 3rd scenario - three entries, consecutive eras, subtract from the second era. - // Only second and third entry should be updated. First one should remain unchanged. - let third_era = 3; - let third_amount = 29; - assert_ok!(vec.add_amount(third_amount, third_era)); - assert_ok!(vec.subtract_amount(sub_amount, second_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount - sub_amount, first_era), - "First entry should remain unchanged, compared to previous scenario." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount + second_amount - sub_amount * 3, second_era), - "Second entry should be reduced by the subtracted amount, compared to previous scenario." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new( - first_amount + second_amount + third_amount - sub_amount * 3, - third_era - ), - "Same as for the second entry." - ); -} - -#[test] -fn sparse_bounded_amount_era_vec_subtract_amount_advanced_non_consecutive_works() { - get_u32_type!(MaxLen, 5); - let mut vec = SparseBoundedAmountEraVec::::new(); - - // 1st scenario - two entries, non-consecutive eras, subtract from the mid era. - // Only the second entry should be updated but a new entry should be created. - let (first_era, second_era) = (1, 5); - let (first_amount, second_amount) = (19, 23); - assert_ok!(vec.add_amount(first_amount, first_era)); - assert_ok!(vec.add_amount(second_amount, second_era)); - - let sub_amount = 3; - let mid_era = second_era - 1; - assert_ok!(vec.subtract_amount(sub_amount, mid_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "No impact on the first entry expected." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount - sub_amount, mid_era), - "Newly created entry should be equal to the first amount, minus what was subtracted." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(vec.0[1].amount + second_amount, second_era), - "Previous 'second' entry should be total added minus the subtracted amount." - ); - - // 2nd scenario - fully unlock the mid-entry to create a zero entry. - assert_ok!(vec.subtract_amount(vec.0[1].amount, mid_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "No impact on the first entry expected." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(0, mid_era), - "Zero entry should be kept since it's in between two non-zero entries." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(second_amount, second_era), - "Only the second staked amount should remain since everything else was unstaked." - ); - - // 3rd scenario - create an additional non-zero chunk as prep for the next scenario. - let pre_mid_era = mid_era - 1; - assert!(pre_mid_era > first_era, "Sanity check."); - assert_ok!(vec.subtract_amount(sub_amount, pre_mid_era)); - assert_eq!(vec.0.len(), 4); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount - sub_amount, pre_mid_era), - "Newly created entry, derives it's initial value from the first entry." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(0, mid_era), - "Zero entry should be kept at this point since it's still between two non-zero entries." - ); - assert_eq!( - vec.0[3], - DummyEraAmount::new(second_amount - sub_amount, second_era), - "Last entry should be further reduced by the newly subtracted amount." - ); - - // 4th scenario - create an additional zero entry, but ensure it's cleaned up correctly. - let final_sub_amount = vec.0[1].amount; - assert_ok!(vec.subtract_amount(final_sub_amount, pre_mid_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "First entry should still remain unchanged." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(0, pre_mid_era), - "The older zero entry should consume the newer ones, hence the pre_mid_era usage" - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(second_amount - sub_amount - final_sub_amount, second_era), - "Last entry should be further reduced by the newly subtracted amount." - ); -} - -#[test] -fn sparse_bounded_amount_era_vec_full_subtract_with_single_future_era() { - get_u32_type!(MaxLen, 5); - - let mut vec = SparseBoundedAmountEraVec::::new(); - - // A scenario where some amount is added, for the first time, for era X. - // Immediately afterward, the same amount is subtracted from era X - 1. - let (era_1, era_2) = (1, 2); - let amount = 19; - assert_ok!(vec.add_amount(amount, era_2)); - - assert_ok!(vec.subtract_amount(amount, era_1)); - assert!( - vec.0.is_empty(), - "Future entry should have been cleaned up." - ); -} - -#[test] -fn sparse_bounded_amount_era_vec_split_left_works() { - get_u32_type!(MaxLen, 4); - - fn new_era_vec(vec: Vec) -> SparseBoundedAmountEraVec { - let vec: Vec = vec - .into_iter() - .map(|idx| DummyEraAmount::new(idx as Balance, idx)) - .collect(); - SparseBoundedAmountEraVec(BoundedVec::try_from(vec).unwrap()) - } - - // 1st scenario: [1,2,6,7] -- split(4) --> [1,2],[5,6,7] - let mut vec = new_era_vec(vec![1, 2, 6, 7]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 2); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(2, 5), - "Amount must come from last entry in the split." - ); - assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); - assert_eq!(vec.0[2], DummyEraAmount::new(7, 7)); - - // 2nd scenario: [1,2] -- split(4) --> [1,2],[5] - let mut vec = new_era_vec(vec![1, 2]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 2); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - - assert_eq!(vec.0.len(), 1); - assert_eq!( - vec.0[0], - DummyEraAmount::new(2, 5), - "Amount must come from last entry in the split." - ); - - // 3rd scenario: [1,2,4,5] -- split(4) --> [1,2,4],[5] - let mut vec = new_era_vec(vec![1, 2, 4, 5]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 3); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - assert_eq!(result.0[2], DummyEraAmount::new(4, 4)); - - assert_eq!(vec.0.len(), 1); - assert_eq!(vec.0[0], DummyEraAmount::new(5, 5)); - - // 4th scenario: [1,2,4,6] -- split(4) --> [1,2,4],[5,6] - let mut vec = new_era_vec(vec![1, 2, 4, 6]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 3); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - assert_eq!(result.0[2], DummyEraAmount::new(4, 4)); - - assert_eq!(vec.0.len(), 2); - assert_eq!( - vec.0[0], - DummyEraAmount::new(4, 5), - "Amount must come from last entry in the split." - ); - assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); - - // 5th scenario: [1,2(0)] -- split(4) --> [1,2],[] - let vec: Vec = vec![(1, 1), (0, 2)] - .into_iter() - .map(|(amount, era)| DummyEraAmount::new(amount as Balance, era)) - .collect(); - let mut vec = SparseBoundedAmountEraVec::<_, MaxLen>(BoundedVec::try_from(vec).unwrap()); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 2); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(0, 2)); - - assert!(vec.0.is_empty()); -} - -#[test] -fn sparse_bounded_amount_era_vec_split_left_fails_with_invalid_era() { - get_u32_type!(MaxLen, 4); - let mut vec = SparseBoundedAmountEraVec::::new(); - assert!(vec.add_amount(5, 5).is_ok()); - - assert_eq!(vec.left_split(4), Err(AccountLedgerError::SplitEraInvalid)); -} - -#[test] -fn sparse_bounded_amount_era_vec_get_works() { - get_u32_type!(MaxLen, 4); - let vec: Vec = vec![2, 3, 5] - .into_iter() - .map(|idx| DummyEraAmount::new(idx as Balance, idx)) - .collect(); - let vec = - SparseBoundedAmountEraVec::(BoundedVec::try_from(vec).unwrap()); - - assert_eq!(vec.get(1), None, "Era is not covered by the vector."); - assert_eq!(vec.get(2), Some(DummyEraAmount::new(2, 2))); - assert_eq!(vec.get(3), Some(DummyEraAmount::new(3, 3))); - assert_eq!( - vec.get(4), - Some(DummyEraAmount::new(3, 4)), - "Era is covered by the 3rd era." - ); - assert_eq!(vec.get(5), Some(DummyEraAmount::new(5, 5))); - assert_eq!( - vec.get(6), - Some(DummyEraAmount::new(5, 6)), - "Era is covered by the 5th era." - ); -} - #[test] fn period_type_sanity_check() { assert_eq!(PeriodType::Voting.next(), PeriodType::BuildAndEarn); @@ -536,8 +128,7 @@ fn protocol_state_basic_checks() { #[test] fn account_ledger_default() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let acc_ledger = AccountLedger::::default(); + let acc_ledger = AccountLedger::::default(); assert!(acc_ledger.is_empty()); assert!(acc_ledger.active_locked_amount().is_zero()); @@ -546,8 +137,7 @@ fn account_ledger_default() { #[test] fn account_ledger_add_lock_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // First step, sanity checks assert!(acc_ledger.active_locked_amount().is_zero()); @@ -566,8 +156,7 @@ fn account_ledger_add_lock_amount_works() { #[test] fn account_ledger_subtract_lock_amount_basic_usage_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -608,8 +197,7 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { #[test] fn account_ledger_add_unlocking_chunk_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -670,35 +258,36 @@ fn account_ledger_add_unlocking_chunk_works() { } #[test] -fn account_ledger_active_stake_works() { +fn account_ledger_staked_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check - assert!(acc_ledger.active_stake(0).is_zero()); - assert!(acc_ledger.active_stake(1).is_zero()); + assert!(acc_ledger.staked_amount(0).is_zero()); + assert!(acc_ledger.staked_amount(1).is_zero()); // Period matches - let amount = 29; + let amount_1 = 29; let period = 5; - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { amount, era: 1 }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(period); - assert_eq!(acc_ledger.active_stake(period), amount); + acc_ledger.staked = StakeAmount::new(amount_1, 0, 1, period); + assert_eq!(acc_ledger.staked_amount(period), amount_1); // Period doesn't match - assert!(acc_ledger.active_stake(period - 1).is_zero()); - assert!(acc_ledger.active_stake(period + 1).is_zero()); + assert!(acc_ledger.staked_amount(period - 1).is_zero()); + assert!(acc_ledger.staked_amount(period + 1).is_zero()); + + // Add future entry + let amount_2 = 17; + acc_ledger.staked_future = Some(StakeAmount::new(0, amount_2, 2, period)); + assert_eq!(acc_ledger.staked_amount(period), amount_2); + assert!(acc_ledger.staked_amount(period - 1).is_zero()); + assert!(acc_ledger.staked_amount(period + 1).is_zero()); } #[test] fn account_ledger_stakeable_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check for empty ledger assert!(acc_ledger.stakeable_amount(1).is_zero()); @@ -716,14 +305,7 @@ fn account_ledger_stakeable_amount_works() { // Second scenario - some staked amount is introduced, period is still valid let first_era = 1; let staked_amount = 7; - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: staked_amount, - era: first_era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(first_period); + acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, first_period); assert_eq!( acc_ledger.stakeable_amount(first_period), @@ -739,1221 +321,1226 @@ fn account_ledger_stakeable_amount_works() { ); } -#[test] -fn account_ledger_staked_amount_works() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Sanity check for empty ledger - assert!(acc_ledger.staked_amount(1).is_zero()); - - // First scenario - active period matches the ledger - let first_era = 1; - let first_period = 1; - let locked_amount = 19; - let staked_amount = 13; - acc_ledger.add_lock_amount(locked_amount); - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: staked_amount, - era: first_era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(first_period); - - assert_eq!(acc_ledger.staked_amount(first_period), staked_amount); - - // Second scenario - active period doesn't match the ledger - assert!(acc_ledger.staked_amount(first_period + 1).is_zero()); -} - #[test] fn account_ledger_add_stake_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check - assert!(acc_ledger.add_stake_amount(0, 0, 0).is_ok()); - assert!(acc_ledger.staked_period.is_none()); - assert!(acc_ledger.staked.0.is_empty()); + assert!(acc_ledger + .add_stake_amount(0, 0, PeriodInfo::new(0, PeriodType::Voting, 0)) + .is_ok()); + assert!(acc_ledger.staked.is_empty()); + assert!(acc_ledger.staked_future.is_none()); - // First scenario - stake some amount, and ensure values are as expected - let first_era = 2; + // First scenario - stake some amount, and ensure values are as expected. + let first_era = 1; let first_period = 1; + let period_info_1 = PeriodInfo::new(first_period, PeriodType::Voting, 100); let lock_amount = 17; let stake_amount = 11; acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, first_period) + .add_stake_amount(stake_amount, first_era, period_info_1) .is_ok()); - assert_eq!(acc_ledger.staked_period, Some(first_period)); - assert_eq!(acc_ledger.staked.0.len(), 1); + + assert!( + acc_ledger.staked.is_empty(), + "Current era must remain unchanged." + ); assert_eq!( - acc_ledger.staked.0[0], - StakeChunk { - amount: stake_amount, - era: first_era, - } + acc_ledger + .staked_future + .expect("Must exist after stake.") + .period, + first_period ); + assert_eq!(acc_ledger.staked_future.unwrap().voting, stake_amount); + assert!(acc_ledger.staked_future.unwrap().build_and_earn.is_zero()); assert_eq!(acc_ledger.staked_amount(first_period), stake_amount); - - // Second scenario - stake some more to the same era, only amount should change + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, first_period), + stake_amount + ); assert!(acc_ledger - .add_stake_amount(1, first_era, first_period) - .is_ok()); - assert_eq!(acc_ledger.staked.0.len(), 1); - assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + .staked_amount_for_type(PeriodType::BuildAndEarn, first_period) + .is_zero()); - // Third scenario - stake to the next era, new chunk should be added - let next_era = first_era + 3; - let remaining_not_staked = lock_amount - stake_amount - 1; + // Second scenario - stake some more to the same era + let snapshot = acc_ledger.staked; assert!(acc_ledger - .add_stake_amount(remaining_not_staked, next_era, first_period) + .add_stake_amount(1, first_era, period_info_1) .is_ok()); - assert_eq!(acc_ledger.staked.0.len(), 2); - assert_eq!(acc_ledger.staked_amount(first_period), lock_amount); + assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + assert_eq!(acc_ledger.staked, snapshot); } #[test] fn account_ledger_add_stake_amount_invalid_era_fails() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let first_era = 5; let first_period = 2; + let period_info_1 = PeriodInfo::new(first_period, PeriodType::Voting, 100); let lock_amount = 13; let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, first_period) + .add_stake_amount(stake_amount, first_era, period_info_1) .is_ok()); let acc_ledger_snapshot = acc_ledger.clone(); - // Try to add to the next era, it should fail + // Try to add to the next era, it should fail. + println!("{:?}", acc_ledger); assert_eq!( - acc_ledger.add_stake_amount(1, first_era, first_period + 1), - Err(AccountLedgerError::InvalidPeriod) + acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); assert_eq!( acc_ledger, acc_ledger_snapshot, "Previous failed action must be a noop" ); - // Try to add to the previous era, it should fail + // Try to add to the previous era, it should fail. assert_eq!( - acc_ledger.add_stake_amount(1, first_era, first_period - 1), - Err(AccountLedgerError::InvalidPeriod) + acc_ledger.add_stake_amount(1, first_era - 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); assert_eq!( acc_ledger, acc_ledger_snapshot, "Previous failed action must be a noop" ); -} - -#[test] -fn account_ledger_add_stake_amount_too_large_amount_fails() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Sanity check - assert_eq!( - acc_ledger.add_stake_amount(10, 1, 1), - Err(AccountLedgerError::UnavailableStakeFunds) - ); - - // Lock some amount, and try to stake more than that - let first_era = 5; - let first_period = 2; - let lock_amount = 13; - acc_ledger.add_lock_amount(lock_amount); - assert_eq!( - acc_ledger.add_stake_amount(lock_amount + 1, first_era, first_period), - Err(AccountLedgerError::UnavailableStakeFunds) - ); - - // Additional check - have some active stake, and then try to overstake - assert!(acc_ledger - .add_stake_amount(lock_amount - 2, first_era, first_period) - .is_ok()); - assert_eq!( - acc_ledger.add_stake_amount(3, first_era, first_period), - Err(AccountLedgerError::UnavailableStakeFunds) - ); -} - -#[test] -fn account_ledger_add_stake_amount_while_exceeding_capacity_fails() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Try to stake up to the capacity, it should work - // Lock some amount, and try to stake more than that - let first_era = 5; - let first_period = 2; - let lock_amount = 31; - let stake_amount = 3; - acc_ledger.add_lock_amount(lock_amount); - for inc in 0..StakingDummy::get() { - assert!(acc_ledger - .add_stake_amount(stake_amount, first_era + inc, first_period) - .is_ok()); - assert_eq!( - acc_ledger.staked_amount(first_period), - stake_amount * (inc as u128 + 1) - ); - } - - // Can still stake to the last staked era - assert!(acc_ledger - .add_stake_amount( - stake_amount, - first_era + StakingDummy::get() - 1, - first_period - ) - .is_ok()); - - // But staking to the next era must fail with exceeded capacity - assert_eq!( - acc_ledger.add_stake_amount(stake_amount, first_era + StakingDummy::get(), first_period), - Err(AccountLedgerError::NoCapacity) - ); -} - -#[test] -fn account_ledger_unstake_amount_works() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Prep actions - let amount_1 = 19; - let era_1 = 2; - let period_1 = 1; - acc_ledger.add_lock_amount(amount_1); - assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) - .is_ok()); - - // Sanity check - assert!(acc_ledger.unstake_amount(0, era_1, period_1).is_ok()); - - // 1st scenario - unstake some amount from the current era. - let unstake_amount_1 = 3; - assert!(acc_ledger - .unstake_amount(unstake_amount_1, era_1, period_1) - .is_ok()); - assert_eq!( - acc_ledger.staked_amount(period_1), - amount_1 - unstake_amount_1 - ); - assert_eq!( - acc_ledger.staked.0.len(), - 1, - "Only existing entry should be updated." - ); - - // 2nd scenario - unstake some more, but from the next era - let era_2 = era_1 + 1; - assert!(acc_ledger - .unstake_amount(unstake_amount_1, era_2, period_1) - .is_ok()); - assert_eq!( - acc_ledger.staked_amount(period_1), - amount_1 - unstake_amount_1 * 2 - ); - assert_eq!( - acc_ledger.staked.0.len(), - 2, - "New entry must be created to cover the new era stake." - ); - // 3rd scenario - unstake some more, bump era by a larger number - let era_3 = era_2 + 3; - assert!(acc_ledger - .unstake_amount(unstake_amount_1, era_3, period_1) - .is_ok()); + // Try to add to the next period, it should fail. assert_eq!( - acc_ledger.staked_amount(period_1), - amount_1 - unstake_amount_1 * 3 + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(first_period + 1, PeriodType::Voting, 100) + ), + Err(AccountLedgerError::InvalidPeriod) ); assert_eq!( - acc_ledger.staked.0.len(), - 3, - "New entry must be created to cover the new era stake." + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" ); -} - -#[test] -fn account_ledger_unstake_from_invalid_era_fails() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Prep actions - let amount_1 = 13; - let era_1 = 2; - let period_1 = 1; - acc_ledger.add_lock_amount(amount_1); - assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) - .is_ok()); + // Try to add to the previous period, it should fail assert_eq!( - acc_ledger.unstake_amount(amount_1, era_1 + 1, period_1 + 1), + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(first_period - 1, PeriodType::Voting, 100) + ), Err(AccountLedgerError::InvalidPeriod) ); -} - -#[test] -fn account_ledger_unstake_too_much_fails() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Prep actions - let amount_1 = 23; - let era_1 = 2; - let period_1 = 1; - acc_ledger.add_lock_amount(amount_1); - assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) - .is_ok()); - assert_eq!( - acc_ledger.unstake_amount(amount_1 + 1, era_1, period_1), - Err(AccountLedgerError::UnstakeAmountLargerThanStake) + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" ); } -#[test] -fn account_ledger_unstake_exceeds_capacity() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Prep actions - let amount_1 = 100; - let era_1 = 2; - let period_1 = 1; - acc_ledger.add_lock_amount(amount_1); - assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) - .is_ok()); - - for x in 0..StakingDummy::get() { - assert!( - acc_ledger.unstake_amount(3, era_1 + x, period_1).is_ok(), - "Capacity isn't full so unstake must work." - ); - } - - assert_eq!( - acc_ledger.unstake_amount(3, era_1 + StakingDummy::get(), period_1), - Err(AccountLedgerError::NoCapacity) - ); -} - -#[test] -fn account_ledger_unlockable_amount_works() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Sanity check scenario - assert!(acc_ledger.unlockable_amount(0).is_zero()); - - // Nothing is staked - let lock_amount = 29; - let lock_era = 3; - acc_ledger.add_lock_amount(lock_amount); - assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); - - // Some amount is staked, period matches - let stake_period = 5; - let stake_amount = 17; - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: stake_amount, - era: lock_era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(stake_period); - assert_eq!( - acc_ledger.unlockable_amount(stake_period), - lock_amount - stake_amount - ); - - // Period doesn't match - assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); - assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); - - // Absurd example, for the sake of completeness - staked without any lock - acc_ledger.locked = Balance::zero(); - assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); - assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); - assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); -} - -#[test] -fn account_ledger_claim_unlocked_works() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Sanity check scenario - assert!(acc_ledger.claim_unlocked(0).is_zero()); - - // Add a chunk, assert it can be claimed correctly - let amount = 19; - let block_number = 1; - assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); - assert!(acc_ledger.claim_unlocked(0).is_zero()); - assert_eq!(acc_ledger.claim_unlocked(block_number), amount); - assert!(acc_ledger.unlocking.is_empty()); - - // Add multiple chunks, assert claim works correctly - let (amount1, amount2, amount3) = (7, 13, 19); - let (block1, block2, block3) = (1, 3, 5); - - // Prepare unlocking chunks - assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); - assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); - assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); - - // Only claim 1 chunk - assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); - assert_eq!(acc_ledger.unlocking.len(), 2); - - // Claim remaining two chunks - assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); - assert!(acc_ledger.unlocking.is_empty()); -} - -#[test] -fn account_ledger_consume_unlocking_chunks_works() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Sanity check scenario - assert!(acc_ledger.consume_unlocking_chunks().is_zero()); - - // Add multiple chunks, cal should return correct amount - let (amount1, amount2) = (7, 13); - assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); - assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); - - assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); - assert!(acc_ledger.unlocking.is_empty()); -} - -#[test] -fn era_info_lock_unlock_works() { - let mut era_info = EraInfo::default(); - - // Sanity check - assert!(era_info.total_locked.is_zero()); - assert!(era_info.active_era_locked.is_zero()); - assert!(era_info.unlocking.is_zero()); - - // Basic add lock - let lock_amount = 7; - era_info.add_locked(lock_amount); - assert_eq!(era_info.total_locked, lock_amount); - era_info.add_locked(lock_amount); - assert_eq!(era_info.total_locked, lock_amount * 2); - - // Basic unlocking started - let unlock_amount = 2; - era_info.total_locked = 17; - era_info.active_era_locked = 13; - let era_info_snapshot = era_info; - - // First unlock & checks - era_info.unlocking_started(unlock_amount); - assert_eq!( - era_info.total_locked, - era_info_snapshot.total_locked - unlock_amount - ); - assert_eq!( - era_info.active_era_locked, - era_info_snapshot.active_era_locked - unlock_amount - ); - assert_eq!(era_info.unlocking, unlock_amount); - - // Second unlock and checks - era_info.unlocking_started(unlock_amount); - assert_eq!( - era_info.total_locked, - era_info_snapshot.total_locked - unlock_amount * 2 - ); - assert_eq!( - era_info.active_era_locked, - era_info_snapshot.active_era_locked - unlock_amount * 2 - ); - assert_eq!(era_info.unlocking, unlock_amount * 2); - - // Claim unlocked chunks - let old_era_info = era_info.clone(); - era_info.unlocking_removed(1); - assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); - assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); -} - -#[test] -fn era_info_stake_works() { - let mut era_info = EraInfo::default(); - - // Sanity check - assert!(era_info.total_locked.is_zero()); - - // Add some voting period stake - let vp_stake_amount = 7; - era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); - assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); - assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), - vp_stake_amount - ); - assert!( - era_info.total_staked_amount().is_zero(), - "Calling stake makes it available only from the next era." - ); - - // Add some build&earn period stake - let bep_stake_amount = 13; - era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); - assert_eq!( - era_info.total_staked_amount_next_era(), - vp_stake_amount + bep_stake_amount - ); - assert_eq!( - era_info.staked_amount_next_era(PeriodType::BuildAndEarn), - bep_stake_amount - ); - assert!( - era_info.total_staked_amount().is_zero(), - "Calling stake makes it available only from the next era." - ); -} - -#[test] -fn era_info_unstake_works() { - let mut era_info = EraInfo::default(); - - // Make dummy era info with stake amounts - let vp_stake_amount = 15; - let bep_stake_amount_1 = 23; - let bep_stake_amount_2 = bep_stake_amount_1 + 6; - era_info.current_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_1); - era_info.next_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_2); - let total_staked = era_info.total_staked_amount(); - let total_staked_next_era = era_info.total_staked_amount_next_era(); - - // 1st scenario - unstake some amount, no overflow - let unstake_amount_1 = bep_stake_amount_1; - era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); - - // Current era - assert_eq!( - era_info.total_staked_amount(), - total_staked - unstake_amount_1 - ); - assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); - assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); - - // Next era - assert_eq!( - era_info.total_staked_amount_next_era(), - total_staked_next_era - unstake_amount_1 - ); - assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), - vp_stake_amount - ); - assert_eq!( - era_info.staked_amount_next_era(PeriodType::BuildAndEarn), - bep_stake_amount_2 - unstake_amount_1 - ); - - // 2nd scenario - unstake some more, but with overflow - let overflow = 2; - let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; - era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); - - // Current era - assert_eq!( - era_info.total_staked_amount(), - total_staked - unstake_amount_1 - unstake_amount_2 - ); - - // Next era - assert_eq!( - era_info.total_staked_amount_next_era(), - vp_stake_amount - overflow - ); - assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), - vp_stake_amount - overflow - ); - assert!(era_info - .staked_amount_next_era(PeriodType::BuildAndEarn) - .is_zero()); -} - -#[test] -fn stake_amount_works() { - let mut stake_amount = StakeAmount::default(); - - // Sanity check - assert!(stake_amount.total().is_zero()); - assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); - - // Stake some amount in voting period - let vp_stake_1 = 11; - stake_amount.stake(vp_stake_1, PeriodType::Voting); - assert_eq!(stake_amount.total(), vp_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); - - // Stake some amount in build&earn period - let bep_stake_1 = 13; - stake_amount.stake(bep_stake_1, PeriodType::BuildAndEarn); - assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); - - // Unstake some amount from voting period - let vp_unstake_1 = 5; - stake_amount.unstake(5, PeriodType::Voting); - assert_eq!( - stake_amount.total(), - vp_stake_1 + bep_stake_1 - vp_unstake_1 - ); - assert_eq!( - stake_amount.for_type(PeriodType::Voting), - vp_stake_1 - vp_unstake_1 - ); - assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); - - // Unstake some amount from build&earn period - let bep_unstake_1 = 2; - stake_amount.unstake(bep_unstake_1, PeriodType::BuildAndEarn); - assert_eq!( - stake_amount.total(), - vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 - ); - assert_eq!( - stake_amount.for_type(PeriodType::Voting), - vp_stake_1 - vp_unstake_1 - ); - assert_eq!( - stake_amount.for_type(PeriodType::BuildAndEarn), - bep_stake_1 - bep_unstake_1 - ); - - // Unstake some more from build&earn period, and chip away from the voting period - let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; - let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; - stake_amount.unstake(bep_unstake_2, PeriodType::BuildAndEarn); - assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); - assert_eq!( - stake_amount.for_type(PeriodType::Voting), - vp_stake_1 - vp_unstake_1 - 1 - ); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); -} - -#[test] -fn singular_staking_info_basics_are_ok() { - let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); - - // Sanity checks - assert_eq!(staking_info.period_number(), period_number); - assert!(staking_info.is_loyal()); - assert!(staking_info.total_staked_amount().is_zero()); - assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); - - // Add some staked amount during `Voting` period - let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); - assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); - assert_eq!( - staking_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert!(staking_info - .staked_amount(PeriodType::BuildAndEarn) - .is_zero()); - - // Add some staked amount during `BuildAndEarn` period - let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); - assert_eq!( - staking_info.total_staked_amount(), - vote_stake_amount_1 + bep_stake_amount_1 - ); - assert_eq!( - staking_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert_eq!( - staking_info.staked_amount(PeriodType::BuildAndEarn), - bep_stake_amount_1 - ); -} - -#[test] -fn singular_staking_info_unstake_during_voting_is_ok() { - let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); - - // Prep actions - let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); - - // Unstake some amount during `Voting` period, loyalty should remain as expected. - let unstake_amount_1 = 5; - assert_eq!( - staking_info.unstake(unstake_amount_1, PeriodType::Voting), - (unstake_amount_1, Balance::zero()) - ); - assert_eq!( - staking_info.total_staked_amount(), - vote_stake_amount_1 - unstake_amount_1 - ); - assert!(staking_info.is_loyal()); - - // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. - let remaining_stake = staking_info.total_staked_amount(); - assert_eq!( - staking_info.unstake(remaining_stake + 1, PeriodType::Voting), - (remaining_stake, Balance::zero()) - ); - assert!(staking_info.total_staked_amount().is_zero()); - assert!(staking_info.is_loyal()); -} - -#[test] -fn singular_staking_info_unstake_during_bep_is_ok() { - let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); - - // Prep actions - let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); - let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); - - // 1st scenario - Unstake some of the amount staked during B&E period - let unstake_1 = 5; - assert_eq!( - staking_info.unstake(5, PeriodType::BuildAndEarn), - (Balance::zero(), unstake_1) - ); - assert_eq!( - staking_info.total_staked_amount(), - vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 - ); - assert_eq!( - staking_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert_eq!( - staking_info.staked_amount(PeriodType::BuildAndEarn), - bep_stake_amount_1 - unstake_1 - ); - assert!(staking_info.is_loyal()); - - // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. - // The point is to take a chunk from the voting period stake too. - let current_total_stake = staking_info.total_staked_amount(); - let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); - let voting_stake_overflow = 2; - let unstake_2 = current_bep_stake + voting_stake_overflow; - - assert_eq!( - staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), - (voting_stake_overflow, current_bep_stake) - ); - assert_eq!( - staking_info.total_staked_amount(), - current_total_stake - unstake_2 - ); - assert_eq!( - staking_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - voting_stake_overflow - ); - assert!(staking_info - .staked_amount(PeriodType::BuildAndEarn) - .is_zero()); - assert!( - !staking_info.is_loyal(), - "Loyalty flag should have been removed due to non-zero voting period unstake" - ); -} - -#[test] -fn contract_stake_info_is_ok() { - let period = 2; - let era = 3; - let mut contract_stake_info = ContractStakingInfo::new(era, period); - - // Sanity check - assert_eq!(contract_stake_info.period(), period); - assert_eq!(contract_stake_info.era(), era); - assert!(contract_stake_info.total_staked_amount().is_zero()); - assert!(contract_stake_info.is_empty()); - - // 1st scenario - Add some staked amount to the voting period - let vote_stake_amount_1 = 11; - contract_stake_info.stake(vote_stake_amount_1, PeriodType::Voting); - assert_eq!( - contract_stake_info.total_staked_amount(), - vote_stake_amount_1 - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert!(!contract_stake_info.is_empty()); - - // 2nd scenario - add some staked amount to the B&E period - let bep_stake_amount_1 = 23; - contract_stake_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); - assert_eq!( - contract_stake_info.total_staked_amount(), - vote_stake_amount_1 + bep_stake_amount_1 - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::BuildAndEarn), - bep_stake_amount_1 - ); - - // 3rd scenario - reduce some of the staked amount from both periods and verify it's as expected. - let total_staked = contract_stake_info.total_staked_amount(); - let vp_reduction = 3; - contract_stake_info.unstake(vp_reduction, PeriodType::Voting); - assert_eq!( - contract_stake_info.total_staked_amount(), - total_staked - vp_reduction - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - vp_reduction - ); - - let bp_reduction = 7; - contract_stake_info.unstake(bp_reduction, PeriodType::BuildAndEarn); - assert_eq!( - contract_stake_info.total_staked_amount(), - total_staked - vp_reduction - bp_reduction - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::BuildAndEarn), - bep_stake_amount_1 - bp_reduction - ); - - // 4th scenario - unstake everything, and some more, from Build&Earn period, chiping away from the voting period. - let overflow = 1; - let overflow_reduction = contract_stake_info.staked_amount(PeriodType::BuildAndEarn) + overflow; - contract_stake_info.unstake(overflow_reduction, PeriodType::BuildAndEarn); - assert_eq!( - contract_stake_info.total_staked_amount(), - vote_stake_amount_1 - vp_reduction - overflow - ); - assert!(contract_stake_info - .staked_amount(PeriodType::BuildAndEarn) - .is_zero()); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - vp_reduction - overflow - ); -} - -#[test] -fn contract_staking_info_series_get_works() { - let info_1 = ContractStakingInfo::new(4, 2); - let mut info_2 = ContractStakingInfo::new(7, 3); - info_2.stake(11, PeriodType::Voting); - let mut info_3 = ContractStakingInfo::new(9, 3); - info_3.stake(13, PeriodType::BuildAndEarn); - - let series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); - - // Sanity check - assert_eq!(series.len(), 3); - assert!(!series.is_empty()); - - // 1st scenario - get existing entries - assert_eq!(series.get(4, 2), Some(info_1)); - assert_eq!(series.get(7, 3), Some(info_2)); - assert_eq!(series.get(9, 3), Some(info_3)); - - // 2nd scenario - get non-existing entries for covered eras - { - let era_1 = 6; - let entry_1 = series.get(era_1, 2).expect("Has to be Some"); - assert!(entry_1.total_staked_amount().is_zero()); - assert_eq!(entry_1.era(), era_1); - assert_eq!(entry_1.period(), 2); - - let era_2 = 8; - let entry_1 = series.get(era_2, 3).expect("Has to be Some"); - assert_eq!(entry_1.total_staked_amount(), 11); - assert_eq!(entry_1.era(), era_2); - assert_eq!(entry_1.period(), 3); - } - - // 3rd scenario - get non-existing entries for covered eras but mismatching period - assert!(series.get(8, 2).is_none()); - - // 4th scenario - get non-existing entries for non-covered eras - assert!(series.get(3, 2).is_none()); -} - -#[test] -fn contract_staking_info_series_stake_is_ok() { - let mut series = ContractStakingInfoSeries::default(); - - // Sanity check - assert!(series.is_empty()); - assert!(series.len().is_zero()); - - // 1st scenario - stake some amount and verify state change - let era_1 = 3; - let period_1 = 5; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); - let amount_1 = 31; - assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); - - assert_eq!(series.len(), 1); - assert!(!series.is_empty()); - - let entry_1_1 = series.get(era_1, period_1).unwrap(); - assert_eq!(entry_1_1.era(), era_1); - assert_eq!(entry_1_1.total_staked_amount(), amount_1); - - // 2nd scenario - stake some more to the same era but different period type, and verify state change. - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); - assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); - assert_eq!( - series.len(), - 1, - "No new entry should be created since it's the same era." - ); - let entry_1_2 = series.get(era_1, period_1).unwrap(); - assert_eq!(entry_1_2.era(), era_1); - assert_eq!(entry_1_2.total_staked_amount(), amount_1 * 2); - - // 3rd scenario - stake more to the next era, while still in the same period. - let era_2 = era_1 + 2; - let amount_2 = 37; - assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); - assert_eq!(series.len(), 2); - let entry_2_1 = series.get(era_1, period_1).unwrap(); - let entry_2_2 = series.get(era_2, period_1).unwrap(); - assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); - assert_eq!(entry_2_2.era(), era_2); - assert_eq!(entry_2_2.period(), period_1); - assert_eq!( - entry_2_2.total_staked_amount(), - entry_2_1.total_staked_amount() + amount_2, - "Since it's the same period, stake amount must carry over from the previous entry." - ); - - // 4th scenario - stake some more to the next era, but this time also bump the period. - let era_3 = era_2 + 3; - let period_2 = period_1 + 1; - let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); - let amount_3 = 41; - - assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); - assert_eq!(series.len(), 3); - let entry_3_1 = series.get(era_1, period_1).unwrap(); - let entry_3_2 = series.get(era_2, period_1).unwrap(); - let entry_3_3 = series.get(era_3, period_2).unwrap(); - assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); - assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); - assert_eq!(entry_3_3.era(), era_3); - assert_eq!(entry_3_3.period(), period_2); - assert_eq!( - entry_3_3.total_staked_amount(), - amount_3, - "No carry over from previous entry since period has changed." - ); - - // 5th scenario - stake to the next era, expect cleanup of oldest entry - let era_4 = era_3 + 1; - let amount_4 = 5; - assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); - assert_eq!(series.len(), 3); - let entry_4_1 = series.get(era_2, period_1).unwrap(); - let entry_4_2 = series.get(era_3, period_2).unwrap(); - let entry_4_3 = series.get(era_4, period_2).unwrap(); - assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); - assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); - assert_eq!(entry_4_3.era(), era_4); - assert_eq!(entry_4_3.period(), period_2); - assert_eq!(entry_4_3.total_staked_amount(), amount_3 + amount_4); -} - -#[test] -fn contract_staking_info_series_stake_with_inconsistent_data_fails() { - let mut series = ContractStakingInfoSeries::default(); - - // Create an entry with some staked amount - let era = 5; - let period_info = PeriodInfo { - number: 7, - period_type: PeriodType::Voting, - ending_era: 31, - }; - let amount = 37; - assert!(series.stake(amount, period_info, era).is_ok()); - - // 1st scenario - attempt to stake using old era - assert!(series.stake(amount, period_info, era - 1).is_err()); - - // 2nd scenario - attempt to stake using old period - let period_info = PeriodInfo { - number: period_info.number - 1, - period_type: PeriodType::Voting, - ending_era: 31, - }; - assert!(series.stake(amount, period_info, era).is_err()); -} - -#[test] -fn contract_staking_info_series_unstake_is_ok() { - let mut series = ContractStakingInfoSeries::default(); - - // Prep action - create a stake entry - let era_1 = 2; - let period = 3; - let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); - let stake_amount = 100; - assert!(series.stake(stake_amount, period_info, era_1).is_ok()); - - // 1st scenario - unstake in the same era - let amount_1 = 5; - assert!(series.unstake(amount_1, period_info, era_1).is_ok()); - assert_eq!(series.len(), 1); - assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); - assert_eq!( - series.staked_amount(period, PeriodType::Voting), - stake_amount - amount_1 - ); - - // 2nd scenario - unstake in the future era, creating a 'gap' in the series - // [(era: 2)] ---> [(era: 2), (era: 5)] - let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); - let era_2 = era_1 + 3; - let amount_2 = 7; - assert!(series.unstake(amount_2, period_info, era_2).is_ok()); - assert_eq!(series.len(), 2); - assert_eq!( - series.total_staked_amount(period), - stake_amount - amount_1 - amount_2 - ); - assert_eq!( - series.staked_amount(period, PeriodType::Voting), - stake_amount - amount_1 - amount_2 - ); - - // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones - // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] - let era_3 = era_2 - 1; - let amount_3 = 11; - assert!(series.unstake(amount_3, period_info, era_3).is_ok()); - assert_eq!(series.len(), 3); - assert_eq!( - series.total_staked_amount(period), - stake_amount - amount_1 - amount_2 - amount_3 - ); - assert_eq!( - series.staked_amount(period, PeriodType::Voting), - stake_amount - amount_1 - amount_2 - amount_3 - ); - - // Check concrete entries - assert_eq!( - series.get(era_1, period).unwrap().total_staked_amount(), - stake_amount - amount_1, - "Oldest entry must remain unchanged." - ); - assert_eq!( - series.get(era_2, period).unwrap().total_staked_amount(), - stake_amount - amount_1 - amount_2 - amount_3, - "Future era entry must be updated with all of the reductions." - ); - assert_eq!( - series.get(era_3, period).unwrap().total_staked_amount(), - stake_amount - amount_1 - amount_3, - "Second to last era entry must be updated with first & last reduction\ - because it derives its initial value from the oldest entry." - ); -} - -#[test] -fn contract_staking_info_unstake_with_worst_case_scenario_for_capacity_overflow() { - let (era_1, era_2, era_3) = (4, 7, 9); - let (period_1, period_2) = (2, 3); - let info_1 = ContractStakingInfo::new(era_1, period_1); - let mut info_2 = ContractStakingInfo::new(era_2, period_2); - let stake_amount_2 = 11; - info_2.stake(stake_amount_2, PeriodType::Voting); - let mut info_3 = ContractStakingInfo::new(era_3, period_2); - let stake_amount_3 = 13; - info_3.stake(stake_amount_3, PeriodType::BuildAndEarn); - - // A gap between 2nd and 3rd era, and from that gap unstake will be done. - // This will force a new entry to be created, potentially overflowing the vector capacity. - let mut series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); - - // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity - let period_info = PeriodInfo { - number: period_2, - period_type: PeriodType::BuildAndEarn, - ending_era: 51, - }; - let unstake_amount = 3; - assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); - assert_eq!(series.len(), 3); - - assert_eq!( - series.get(era_1, period_1), - None, - "Oldest entry should have been prunned" - ); - assert_eq!( - series - .get(era_2, period_2) - .expect("Entry must exist.") - .total_staked_amount(), - stake_amount_2 - ); - assert_eq!( - series - .get(era_2 + 1, period_2) - .expect("Entry must exist.") - .total_staked_amount(), - stake_amount_2 - unstake_amount - ); - assert_eq!( - series - .get(era_3, period_2) - .expect("Entry must exist.") - .total_staked_amount(), - stake_amount_3 - unstake_amount - ); -} - -#[test] -fn contract_staking_info_series_unstake_with_inconsistent_data_fails() { - let mut series = ContractStakingInfoSeries::default(); - let era = 5; - let period = 2; - let period_info = PeriodInfo { - number: period, - period_type: PeriodType::Voting, - ending_era: 31, - }; - - // 1st - Unstake from empty series - assert!(series.unstake(1, period_info, era).is_err()); - - // 2nd - Unstake with old period - let amount = 37; - assert!(series.stake(amount, period_info, era).is_ok()); - - let old_period_info = { - let mut temp = period_info.clone(); - temp.number -= 1; - temp - }; - assert!(series.unstake(1, old_period_info, era - 1).is_err()); - - // 3rd - Unstake with 'too' old era - assert!(series.unstake(1, period_info, era - 2).is_err()); - assert!(series.unstake(1, period_info, era - 1).is_ok()); -} - -#[test] -fn era_reward_span_push_and_get_works() { - get_u32_type!(SpanLength, 8); - let mut era_reward_span = EraRewardSpan::::new(); - - // Sanity checks - assert!(era_reward_span.is_empty()); - assert!(era_reward_span.len().is_zero()); - assert!(era_reward_span.first_era().is_zero()); - assert!(era_reward_span.last_era().is_zero()); - - // Insert some values and verify state change - let era_1 = 5; - let era_reward_1 = EraReward::new(23, 41); - assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); - assert_eq!(era_reward_span.len(), 1); - assert_eq!(era_reward_span.first_era(), era_1); - assert_eq!(era_reward_span.last_era(), era_1); - - // Insert another value and verify state change - let era_2 = era_1 + 1; - let era_reward_2 = EraReward::new(37, 53); - assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); - assert_eq!(era_reward_span.len(), 2); - assert_eq!(era_reward_span.first_era(), era_1); - assert_eq!(era_reward_span.last_era(), era_2); - - // Get the values and verify they are as expected - assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); - assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); -} - -#[test] -fn era_reward_span_fails_when_expected() { - // Capacity is only 2 to make testing easier - get_u32_type!(SpanLength, 2); - let mut era_reward_span = EraRewardSpan::::new(); - - // Push first values to get started - let era_1 = 5; - let era_reward = EraReward::new(23, 41); - assert!(era_reward_span.push(era_1, era_reward).is_ok()); - - // Attempting to push incorrect era results in an error - for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { - assert_eq!( - era_reward_span.push(*wrong_era, era_reward), - Err(EraRewardSpanError::InvalidEra) - ); - } - - // Pushing above capacity results in an error - let era_2 = era_1 + 1; - assert!(era_reward_span.push(era_2, era_reward).is_ok()); - let era_3 = era_2 + 1; - assert_eq!( - era_reward_span.push(era_3, era_reward), - Err(EraRewardSpanError::NoCapacity) - ); -} +// #[test] +// fn account_ledger_add_stake_amount_too_large_amount_fails() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Sanity check +// assert_eq!( +// acc_ledger.add_stake_amount(10, 1, 1), +// Err(AccountLedgerError::UnavailableStakeFunds) +// ); + +// // Lock some amount, and try to stake more than that +// let first_era = 5; +// let first_period = 2; +// let lock_amount = 13; +// acc_ledger.add_lock_amount(lock_amount); +// assert_eq!( +// acc_ledger.add_stake_amount(lock_amount + 1, first_era, first_period), +// Err(AccountLedgerError::UnavailableStakeFunds) +// ); + +// // Additional check - have some active stake, and then try to overstake +// assert!(acc_ledger +// .add_stake_amount(lock_amount - 2, first_era, first_period) +// .is_ok()); +// assert_eq!( +// acc_ledger.add_stake_amount(3, first_era, first_period), +// Err(AccountLedgerError::UnavailableStakeFunds) +// ); +// } + +// #[test] +// fn account_ledger_add_stake_amount_while_exceeding_capacity_fails() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Try to stake up to the capacity, it should work +// // Lock some amount, and try to stake more than that +// let first_era = 5; +// let first_period = 2; +// let lock_amount = 31; +// let stake_amount = 3; +// acc_ledger.add_lock_amount(lock_amount); +// for inc in 0..StakingDummy::get() { +// assert!(acc_ledger +// .add_stake_amount(stake_amount, first_era + inc, first_period) +// .is_ok()); +// assert_eq!( +// acc_ledger.staked_amount(first_period), +// stake_amount * (inc as u128 + 1) +// ); +// } + +// // Can still stake to the last staked era +// assert!(acc_ledger +// .add_stake_amount( +// stake_amount, +// first_era + StakingDummy::get() - 1, +// first_period +// ) +// .is_ok()); + +// // But staking to the next era must fail with exceeded capacity +// assert_eq!( +// acc_ledger.add_stake_amount(stake_amount, first_era + StakingDummy::get(), first_period), +// Err(AccountLedgerError::NoCapacity) +// ); +// } + +// #[test] +// fn account_ledger_unstake_amount_works() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Prep actions +// let amount_1 = 19; +// let era_1 = 2; +// let period_1 = 1; +// acc_ledger.add_lock_amount(amount_1); +// assert!(acc_ledger +// .add_stake_amount(amount_1, era_1, period_1) +// .is_ok()); + +// // Sanity check +// assert!(acc_ledger.unstake_amount(0, era_1, period_1).is_ok()); + +// // 1st scenario - unstake some amount from the current era. +// let unstake_amount_1 = 3; +// assert!(acc_ledger +// .unstake_amount(unstake_amount_1, era_1, period_1) +// .is_ok()); +// assert_eq!( +// acc_ledger.staked_amount(period_1), +// amount_1 - unstake_amount_1 +// ); +// assert_eq!( +// acc_ledger.staked.0.len(), +// 1, +// "Only existing entry should be updated." +// ); + +// // 2nd scenario - unstake some more, but from the next era +// let era_2 = era_1 + 1; +// assert!(acc_ledger +// .unstake_amount(unstake_amount_1, era_2, period_1) +// .is_ok()); +// assert_eq!( +// acc_ledger.staked_amount(period_1), +// amount_1 - unstake_amount_1 * 2 +// ); +// assert_eq!( +// acc_ledger.staked.0.len(), +// 2, +// "New entry must be created to cover the new era stake." +// ); + +// // 3rd scenario - unstake some more, bump era by a larger number +// let era_3 = era_2 + 3; +// assert!(acc_ledger +// .unstake_amount(unstake_amount_1, era_3, period_1) +// .is_ok()); +// assert_eq!( +// acc_ledger.staked_amount(period_1), +// amount_1 - unstake_amount_1 * 3 +// ); +// assert_eq!( +// acc_ledger.staked.0.len(), +// 3, +// "New entry must be created to cover the new era stake." +// ); +// } + +// #[test] +// fn account_ledger_unstake_from_invalid_era_fails() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Prep actions +// let amount_1 = 13; +// let era_1 = 2; +// let period_1 = 1; +// acc_ledger.add_lock_amount(amount_1); +// assert!(acc_ledger +// .add_stake_amount(amount_1, era_1, period_1) +// .is_ok()); + +// assert_eq!( +// acc_ledger.unstake_amount(amount_1, era_1 + 1, period_1 + 1), +// Err(AccountLedgerError::InvalidPeriod) +// ); +// } + +// #[test] +// fn account_ledger_unstake_too_much_fails() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Prep actions +// let amount_1 = 23; +// let era_1 = 2; +// let period_1 = 1; +// acc_ledger.add_lock_amount(amount_1); +// assert!(acc_ledger +// .add_stake_amount(amount_1, era_1, period_1) +// .is_ok()); + +// assert_eq!( +// acc_ledger.unstake_amount(amount_1 + 1, era_1, period_1), +// Err(AccountLedgerError::UnstakeAmountLargerThanStake) +// ); +// } + +// #[test] +// fn account_ledger_unstake_exceeds_capacity() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Prep actions +// let amount_1 = 100; +// let era_1 = 2; +// let period_1 = 1; +// acc_ledger.add_lock_amount(amount_1); +// assert!(acc_ledger +// .add_stake_amount(amount_1, era_1, period_1) +// .is_ok()); + +// for x in 0..StakingDummy::get() { +// assert!( +// acc_ledger.unstake_amount(3, era_1 + x, period_1).is_ok(), +// "Capacity isn't full so unstake must work." +// ); +// } + +// assert_eq!( +// acc_ledger.unstake_amount(3, era_1 + StakingDummy::get(), period_1), +// Err(AccountLedgerError::NoCapacity) +// ); +// } + +// #[test] +// fn account_ledger_unlockable_amount_works() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Sanity check scenario +// assert!(acc_ledger.unlockable_amount(0).is_zero()); + +// // Nothing is staked +// let lock_amount = 29; +// let lock_era = 3; +// acc_ledger.add_lock_amount(lock_amount); +// assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); + +// // Some amount is staked, period matches +// let stake_period = 5; +// let stake_amount = 17; +// acc_ledger.staked = SparseBoundedAmountEraVec( +// BoundedVec::try_from(vec![StakeChunk { +// amount: stake_amount, +// era: lock_era, +// }]) +// .expect("Only one chunk so creation should succeed."), +// ); +// acc_ledger.staked_period = Some(stake_period); +// assert_eq!( +// acc_ledger.unlockable_amount(stake_period), +// lock_amount - stake_amount +// ); + +// // Period doesn't match +// assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); +// assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); + +// // Absurd example, for the sake of completeness - staked without any lock +// acc_ledger.locked = Balance::zero(); +// assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); +// assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); +// assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); +// } + +// #[test] +// fn account_ledger_claim_unlocked_works() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Sanity check scenario +// assert!(acc_ledger.claim_unlocked(0).is_zero()); + +// // Add a chunk, assert it can be claimed correctly +// let amount = 19; +// let block_number = 1; +// assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); +// assert!(acc_ledger.claim_unlocked(0).is_zero()); +// assert_eq!(acc_ledger.claim_unlocked(block_number), amount); +// assert!(acc_ledger.unlocking.is_empty()); + +// // Add multiple chunks, assert claim works correctly +// let (amount1, amount2, amount3) = (7, 13, 19); +// let (block1, block2, block3) = (1, 3, 5); + +// // Prepare unlocking chunks +// assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); +// assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); +// assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); + +// // Only claim 1 chunk +// assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); +// assert_eq!(acc_ledger.unlocking.len(), 2); + +// // Claim remaining two chunks +// assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); +// assert!(acc_ledger.unlocking.is_empty()); +// } + +// #[test] +// fn account_ledger_consume_unlocking_chunks_works() { +// get_u32_type!(UnlockingDummy, 5); +// get_u32_type!(StakingDummy, 8); +// let mut acc_ledger = AccountLedger::::default(); + +// // Sanity check scenario +// assert!(acc_ledger.consume_unlocking_chunks().is_zero()); + +// // Add multiple chunks, cal should return correct amount +// let (amount1, amount2) = (7, 13); +// assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); +// assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); + +// assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); +// assert!(acc_ledger.unlocking.is_empty()); +// } + +// #[test] +// fn era_info_lock_unlock_works() { +// let mut era_info = EraInfo::default(); + +// // Sanity check +// assert!(era_info.total_locked.is_zero()); +// assert!(era_info.active_era_locked.is_zero()); +// assert!(era_info.unlocking.is_zero()); + +// // Basic add lock +// let lock_amount = 7; +// era_info.add_locked(lock_amount); +// assert_eq!(era_info.total_locked, lock_amount); +// era_info.add_locked(lock_amount); +// assert_eq!(era_info.total_locked, lock_amount * 2); + +// // Basic unlocking started +// let unlock_amount = 2; +// era_info.total_locked = 17; +// era_info.active_era_locked = 13; +// let era_info_snapshot = era_info; + +// // First unlock & checks +// era_info.unlocking_started(unlock_amount); +// assert_eq!( +// era_info.total_locked, +// era_info_snapshot.total_locked - unlock_amount +// ); +// assert_eq!( +// era_info.active_era_locked, +// era_info_snapshot.active_era_locked - unlock_amount +// ); +// assert_eq!(era_info.unlocking, unlock_amount); + +// // Second unlock and checks +// era_info.unlocking_started(unlock_amount); +// assert_eq!( +// era_info.total_locked, +// era_info_snapshot.total_locked - unlock_amount * 2 +// ); +// assert_eq!( +// era_info.active_era_locked, +// era_info_snapshot.active_era_locked - unlock_amount * 2 +// ); +// assert_eq!(era_info.unlocking, unlock_amount * 2); + +// // Claim unlocked chunks +// let old_era_info = era_info.clone(); +// era_info.unlocking_removed(1); +// assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); +// assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); +// } + +// #[test] +// fn era_info_stake_works() { +// let mut era_info = EraInfo::default(); + +// // Sanity check +// assert!(era_info.total_locked.is_zero()); + +// // Add some voting period stake +// let vp_stake_amount = 7; +// era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); +// assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); +// assert_eq!( +// era_info.staked_amount_next_era(PeriodType::Voting), +// vp_stake_amount +// ); +// assert!( +// era_info.total_staked_amount().is_zero(), +// "Calling stake makes it available only from the next era." +// ); + +// // Add some build&earn period stake +// let bep_stake_amount = 13; +// era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); +// assert_eq!( +// era_info.total_staked_amount_next_era(), +// vp_stake_amount + bep_stake_amount +// ); +// assert_eq!( +// era_info.staked_amount_next_era(PeriodType::BuildAndEarn), +// bep_stake_amount +// ); +// assert!( +// era_info.total_staked_amount().is_zero(), +// "Calling stake makes it available only from the next era." +// ); +// } + +// #[test] +// fn era_info_unstake_works() { +// let mut era_info = EraInfo::default(); + +// // Make dummy era info with stake amounts +// let vp_stake_amount = 15; +// let bep_stake_amount_1 = 23; +// let bep_stake_amount_2 = bep_stake_amount_1 + 6; +// era_info.current_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_1); +// era_info.next_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_2); +// let total_staked = era_info.total_staked_amount(); +// let total_staked_next_era = era_info.total_staked_amount_next_era(); + +// // 1st scenario - unstake some amount, no overflow +// let unstake_amount_1 = bep_stake_amount_1; +// era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); + +// // Current era +// assert_eq!( +// era_info.total_staked_amount(), +// total_staked - unstake_amount_1 +// ); +// assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); +// assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); + +// // Next era +// assert_eq!( +// era_info.total_staked_amount_next_era(), +// total_staked_next_era - unstake_amount_1 +// ); +// assert_eq!( +// era_info.staked_amount_next_era(PeriodType::Voting), +// vp_stake_amount +// ); +// assert_eq!( +// era_info.staked_amount_next_era(PeriodType::BuildAndEarn), +// bep_stake_amount_2 - unstake_amount_1 +// ); + +// // 2nd scenario - unstake some more, but with overflow +// let overflow = 2; +// let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; +// era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); + +// // Current era +// assert_eq!( +// era_info.total_staked_amount(), +// total_staked - unstake_amount_1 - unstake_amount_2 +// ); + +// // Next era +// assert_eq!( +// era_info.total_staked_amount_next_era(), +// vp_stake_amount - overflow +// ); +// assert_eq!( +// era_info.staked_amount_next_era(PeriodType::Voting), +// vp_stake_amount - overflow +// ); +// assert!(era_info +// .staked_amount_next_era(PeriodType::BuildAndEarn) +// .is_zero()); +// } + +// #[test] +// fn stake_amount_works() { +// let mut stake_amount = StakeAmount::default(); + +// // Sanity check +// assert!(stake_amount.total().is_zero()); +// assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); +// assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + +// // Stake some amount in voting period +// let vp_stake_1 = 11; +// stake_amount.stake(vp_stake_1, PeriodType::Voting); +// assert_eq!(stake_amount.total(), vp_stake_1); +// assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); +// assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + +// // Stake some amount in build&earn period +// let bep_stake_1 = 13; +// stake_amount.stake(bep_stake_1, PeriodType::BuildAndEarn); +// assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); +// assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); +// assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + +// // Unstake some amount from voting period +// let vp_unstake_1 = 5; +// stake_amount.unstake(5, PeriodType::Voting); +// assert_eq!( +// stake_amount.total(), +// vp_stake_1 + bep_stake_1 - vp_unstake_1 +// ); +// assert_eq!( +// stake_amount.for_type(PeriodType::Voting), +// vp_stake_1 - vp_unstake_1 +// ); +// assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + +// // Unstake some amount from build&earn period +// let bep_unstake_1 = 2; +// stake_amount.unstake(bep_unstake_1, PeriodType::BuildAndEarn); +// assert_eq!( +// stake_amount.total(), +// vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 +// ); +// assert_eq!( +// stake_amount.for_type(PeriodType::Voting), +// vp_stake_1 - vp_unstake_1 +// ); +// assert_eq!( +// stake_amount.for_type(PeriodType::BuildAndEarn), +// bep_stake_1 - bep_unstake_1 +// ); + +// // Unstake some more from build&earn period, and chip away from the voting period +// let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; +// let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; +// stake_amount.unstake(bep_unstake_2, PeriodType::BuildAndEarn); +// assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); +// assert_eq!( +// stake_amount.for_type(PeriodType::Voting), +// vp_stake_1 - vp_unstake_1 - 1 +// ); +// assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); +// } + +// #[test] +// fn singular_staking_info_basics_are_ok() { +// let period_number = 3; +// let period_type = PeriodType::Voting; +// let mut staking_info = SingularStakingInfo::new(period_number, period_type); + +// // Sanity checks +// assert_eq!(staking_info.period_number(), period_number); +// assert!(staking_info.is_loyal()); +// assert!(staking_info.total_staked_amount().is_zero()); +// assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); + +// // Add some staked amount during `Voting` period +// let vote_stake_amount_1 = 11; +// staking_info.stake(vote_stake_amount_1, PeriodType::Voting); +// assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); +// assert_eq!( +// staking_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 +// ); +// assert!(staking_info +// .staked_amount(PeriodType::BuildAndEarn) +// .is_zero()); + +// // Add some staked amount during `BuildAndEarn` period +// let bep_stake_amount_1 = 23; +// staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); +// assert_eq!( +// staking_info.total_staked_amount(), +// vote_stake_amount_1 + bep_stake_amount_1 +// ); +// assert_eq!( +// staking_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 +// ); +// assert_eq!( +// staking_info.staked_amount(PeriodType::BuildAndEarn), +// bep_stake_amount_1 +// ); +// } + +// #[test] +// fn singular_staking_info_unstake_during_voting_is_ok() { +// let period_number = 3; +// let period_type = PeriodType::Voting; +// let mut staking_info = SingularStakingInfo::new(period_number, period_type); + +// // Prep actions +// let vote_stake_amount_1 = 11; +// staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + +// // Unstake some amount during `Voting` period, loyalty should remain as expected. +// let unstake_amount_1 = 5; +// assert_eq!( +// staking_info.unstake(unstake_amount_1, PeriodType::Voting), +// (unstake_amount_1, Balance::zero()) +// ); +// assert_eq!( +// staking_info.total_staked_amount(), +// vote_stake_amount_1 - unstake_amount_1 +// ); +// assert!(staking_info.is_loyal()); + +// // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. +// let remaining_stake = staking_info.total_staked_amount(); +// assert_eq!( +// staking_info.unstake(remaining_stake + 1, PeriodType::Voting), +// (remaining_stake, Balance::zero()) +// ); +// assert!(staking_info.total_staked_amount().is_zero()); +// assert!(staking_info.is_loyal()); +// } + +// #[test] +// fn singular_staking_info_unstake_during_bep_is_ok() { +// let period_number = 3; +// let period_type = PeriodType::Voting; +// let mut staking_info = SingularStakingInfo::new(period_number, period_type); + +// // Prep actions +// let vote_stake_amount_1 = 11; +// staking_info.stake(vote_stake_amount_1, PeriodType::Voting); +// let bep_stake_amount_1 = 23; +// staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + +// // 1st scenario - Unstake some of the amount staked during B&E period +// let unstake_1 = 5; +// assert_eq!( +// staking_info.unstake(5, PeriodType::BuildAndEarn), +// (Balance::zero(), unstake_1) +// ); +// assert_eq!( +// staking_info.total_staked_amount(), +// vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 +// ); +// assert_eq!( +// staking_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 +// ); +// assert_eq!( +// staking_info.staked_amount(PeriodType::BuildAndEarn), +// bep_stake_amount_1 - unstake_1 +// ); +// assert!(staking_info.is_loyal()); + +// // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. +// // The point is to take a chunk from the voting period stake too. +// let current_total_stake = staking_info.total_staked_amount(); +// let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); +// let voting_stake_overflow = 2; +// let unstake_2 = current_bep_stake + voting_stake_overflow; + +// assert_eq!( +// staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), +// (voting_stake_overflow, current_bep_stake) +// ); +// assert_eq!( +// staking_info.total_staked_amount(), +// current_total_stake - unstake_2 +// ); +// assert_eq!( +// staking_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 - voting_stake_overflow +// ); +// assert!(staking_info +// .staked_amount(PeriodType::BuildAndEarn) +// .is_zero()); +// assert!( +// !staking_info.is_loyal(), +// "Loyalty flag should have been removed due to non-zero voting period unstake" +// ); +// } + +// #[test] +// fn contract_stake_info_is_ok() { +// let period = 2; +// let era = 3; +// let mut contract_stake_info = ContractStakingInfo::new(era, period); + +// // Sanity check +// assert_eq!(contract_stake_info.period(), period); +// assert_eq!(contract_stake_info.era(), era); +// assert!(contract_stake_info.total_staked_amount().is_zero()); +// assert!(contract_stake_info.is_empty()); + +// // 1st scenario - Add some staked amount to the voting period +// let vote_stake_amount_1 = 11; +// contract_stake_info.stake(vote_stake_amount_1, PeriodType::Voting); +// assert_eq!( +// contract_stake_info.total_staked_amount(), +// vote_stake_amount_1 +// ); +// assert_eq!( +// contract_stake_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 +// ); +// assert!(!contract_stake_info.is_empty()); + +// // 2nd scenario - add some staked amount to the B&E period +// let bep_stake_amount_1 = 23; +// contract_stake_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); +// assert_eq!( +// contract_stake_info.total_staked_amount(), +// vote_stake_amount_1 + bep_stake_amount_1 +// ); +// assert_eq!( +// contract_stake_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 +// ); +// assert_eq!( +// contract_stake_info.staked_amount(PeriodType::BuildAndEarn), +// bep_stake_amount_1 +// ); + +// // 3rd scenario - reduce some of the staked amount from both periods and verify it's as expected. +// let total_staked = contract_stake_info.total_staked_amount(); +// let vp_reduction = 3; +// contract_stake_info.unstake(vp_reduction, PeriodType::Voting); +// assert_eq!( +// contract_stake_info.total_staked_amount(), +// total_staked - vp_reduction +// ); +// assert_eq!( +// contract_stake_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 - vp_reduction +// ); + +// let bp_reduction = 7; +// contract_stake_info.unstake(bp_reduction, PeriodType::BuildAndEarn); +// assert_eq!( +// contract_stake_info.total_staked_amount(), +// total_staked - vp_reduction - bp_reduction +// ); +// assert_eq!( +// contract_stake_info.staked_amount(PeriodType::BuildAndEarn), +// bep_stake_amount_1 - bp_reduction +// ); + +// // 4th scenario - unstake everything, and some more, from Build&Earn period, chiping away from the voting period. +// let overflow = 1; +// let overflow_reduction = contract_stake_info.staked_amount(PeriodType::BuildAndEarn) + overflow; +// contract_stake_info.unstake(overflow_reduction, PeriodType::BuildAndEarn); +// assert_eq!( +// contract_stake_info.total_staked_amount(), +// vote_stake_amount_1 - vp_reduction - overflow +// ); +// assert!(contract_stake_info +// .staked_amount(PeriodType::BuildAndEarn) +// .is_zero()); +// assert_eq!( +// contract_stake_info.staked_amount(PeriodType::Voting), +// vote_stake_amount_1 - vp_reduction - overflow +// ); +// } + +// #[test] +// fn contract_staking_info_series_get_works() { +// let info_1 = ContractStakingInfo::new(4, 2); +// let mut info_2 = ContractStakingInfo::new(7, 3); +// info_2.stake(11, PeriodType::Voting); +// let mut info_3 = ContractStakingInfo::new(9, 3); +// info_3.stake(13, PeriodType::BuildAndEarn); + +// let series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); + +// // Sanity check +// assert_eq!(series.len(), 3); +// assert!(!series.is_empty()); + +// // 1st scenario - get existing entries +// assert_eq!(series.get(4, 2), Some(info_1)); +// assert_eq!(series.get(7, 3), Some(info_2)); +// assert_eq!(series.get(9, 3), Some(info_3)); + +// // 2nd scenario - get non-existing entries for covered eras +// { +// let era_1 = 6; +// let entry_1 = series.get(era_1, 2).expect("Has to be Some"); +// assert!(entry_1.total_staked_amount().is_zero()); +// assert_eq!(entry_1.era(), era_1); +// assert_eq!(entry_1.period(), 2); + +// let era_2 = 8; +// let entry_1 = series.get(era_2, 3).expect("Has to be Some"); +// assert_eq!(entry_1.total_staked_amount(), 11); +// assert_eq!(entry_1.era(), era_2); +// assert_eq!(entry_1.period(), 3); +// } + +// // 3rd scenario - get non-existing entries for covered eras but mismatching period +// assert!(series.get(8, 2).is_none()); + +// // 4th scenario - get non-existing entries for non-covered eras +// assert!(series.get(3, 2).is_none()); +// } + +// #[test] +// fn contract_staking_info_series_stake_is_ok() { +// let mut series = ContractStakingInfoSeries::default(); + +// // Sanity check +// assert!(series.is_empty()); +// assert!(series.len().is_zero()); + +// // 1st scenario - stake some amount and verify state change +// let era_1 = 3; +// let period_1 = 5; +// let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); +// let amount_1 = 31; +// assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); + +// assert_eq!(series.len(), 1); +// assert!(!series.is_empty()); + +// let entry_1_1 = series.get(era_1, period_1).unwrap(); +// assert_eq!(entry_1_1.era(), era_1); +// assert_eq!(entry_1_1.total_staked_amount(), amount_1); + +// // 2nd scenario - stake some more to the same era but different period type, and verify state change. +// let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); +// assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); +// assert_eq!( +// series.len(), +// 1, +// "No new entry should be created since it's the same era." +// ); +// let entry_1_2 = series.get(era_1, period_1).unwrap(); +// assert_eq!(entry_1_2.era(), era_1); +// assert_eq!(entry_1_2.total_staked_amount(), amount_1 * 2); + +// // 3rd scenario - stake more to the next era, while still in the same period. +// let era_2 = era_1 + 2; +// let amount_2 = 37; +// assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); +// assert_eq!(series.len(), 2); +// let entry_2_1 = series.get(era_1, period_1).unwrap(); +// let entry_2_2 = series.get(era_2, period_1).unwrap(); +// assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); +// assert_eq!(entry_2_2.era(), era_2); +// assert_eq!(entry_2_2.period(), period_1); +// assert_eq!( +// entry_2_2.total_staked_amount(), +// entry_2_1.total_staked_amount() + amount_2, +// "Since it's the same period, stake amount must carry over from the previous entry." +// ); + +// // 4th scenario - stake some more to the next era, but this time also bump the period. +// let era_3 = era_2 + 3; +// let period_2 = period_1 + 1; +// let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); +// let amount_3 = 41; + +// assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); +// assert_eq!(series.len(), 3); +// let entry_3_1 = series.get(era_1, period_1).unwrap(); +// let entry_3_2 = series.get(era_2, period_1).unwrap(); +// let entry_3_3 = series.get(era_3, period_2).unwrap(); +// assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); +// assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); +// assert_eq!(entry_3_3.era(), era_3); +// assert_eq!(entry_3_3.period(), period_2); +// assert_eq!( +// entry_3_3.total_staked_amount(), +// amount_3, +// "No carry over from previous entry since period has changed." +// ); + +// // 5th scenario - stake to the next era, expect cleanup of oldest entry +// let era_4 = era_3 + 1; +// let amount_4 = 5; +// assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); +// assert_eq!(series.len(), 3); +// let entry_4_1 = series.get(era_2, period_1).unwrap(); +// let entry_4_2 = series.get(era_3, period_2).unwrap(); +// let entry_4_3 = series.get(era_4, period_2).unwrap(); +// assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); +// assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); +// assert_eq!(entry_4_3.era(), era_4); +// assert_eq!(entry_4_3.period(), period_2); +// assert_eq!(entry_4_3.total_staked_amount(), amount_3 + amount_4); +// } + +// #[test] +// fn contract_staking_info_series_stake_with_inconsistent_data_fails() { +// let mut series = ContractStakingInfoSeries::default(); + +// // Create an entry with some staked amount +// let era = 5; +// let period_info = PeriodInfo { +// number: 7, +// period_type: PeriodType::Voting, +// ending_era: 31, +// }; +// let amount = 37; +// assert!(series.stake(amount, period_info, era).is_ok()); + +// // 1st scenario - attempt to stake using old era +// assert!(series.stake(amount, period_info, era - 1).is_err()); + +// // 2nd scenario - attempt to stake using old period +// let period_info = PeriodInfo { +// number: period_info.number - 1, +// period_type: PeriodType::Voting, +// ending_era: 31, +// }; +// assert!(series.stake(amount, period_info, era).is_err()); +// } + +// #[test] +// fn contract_staking_info_series_unstake_is_ok() { +// let mut series = ContractStakingInfoSeries::default(); + +// // Prep action - create a stake entry +// let era_1 = 2; +// let period = 3; +// let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); +// let stake_amount = 100; +// assert!(series.stake(stake_amount, period_info, era_1).is_ok()); + +// // 1st scenario - unstake in the same era +// let amount_1 = 5; +// assert!(series.unstake(amount_1, period_info, era_1).is_ok()); +// assert_eq!(series.len(), 1); +// assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); +// assert_eq!( +// series.staked_amount(period, PeriodType::Voting), +// stake_amount - amount_1 +// ); + +// // 2nd scenario - unstake in the future era, creating a 'gap' in the series +// // [(era: 2)] ---> [(era: 2), (era: 5)] +// let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); +// let era_2 = era_1 + 3; +// let amount_2 = 7; +// assert!(series.unstake(amount_2, period_info, era_2).is_ok()); +// assert_eq!(series.len(), 2); +// assert_eq!( +// series.total_staked_amount(period), +// stake_amount - amount_1 - amount_2 +// ); +// assert_eq!( +// series.staked_amount(period, PeriodType::Voting), +// stake_amount - amount_1 - amount_2 +// ); + +// // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones +// // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] +// let era_3 = era_2 - 1; +// let amount_3 = 11; +// assert!(series.unstake(amount_3, period_info, era_3).is_ok()); +// assert_eq!(series.len(), 3); +// assert_eq!( +// series.total_staked_amount(period), +// stake_amount - amount_1 - amount_2 - amount_3 +// ); +// assert_eq!( +// series.staked_amount(period, PeriodType::Voting), +// stake_amount - amount_1 - amount_2 - amount_3 +// ); + +// // Check concrete entries +// assert_eq!( +// series.get(era_1, period).unwrap().total_staked_amount(), +// stake_amount - amount_1, +// "Oldest entry must remain unchanged." +// ); +// assert_eq!( +// series.get(era_2, period).unwrap().total_staked_amount(), +// stake_amount - amount_1 - amount_2 - amount_3, +// "Future era entry must be updated with all of the reductions." +// ); +// assert_eq!( +// series.get(era_3, period).unwrap().total_staked_amount(), +// stake_amount - amount_1 - amount_3, +// "Second to last era entry must be updated with first & last reduction\ +// because it derives its initial value from the oldest entry." +// ); +// } + +// #[test] +// fn contract_staking_info_unstake_with_worst_case_scenario_for_capacity_overflow() { +// let (era_1, era_2, era_3) = (4, 7, 9); +// let (period_1, period_2) = (2, 3); +// let info_1 = ContractStakingInfo::new(era_1, period_1); +// let mut info_2 = ContractStakingInfo::new(era_2, period_2); +// let stake_amount_2 = 11; +// info_2.stake(stake_amount_2, PeriodType::Voting); +// let mut info_3 = ContractStakingInfo::new(era_3, period_2); +// let stake_amount_3 = 13; +// info_3.stake(stake_amount_3, PeriodType::BuildAndEarn); + +// // A gap between 2nd and 3rd era, and from that gap unstake will be done. +// // This will force a new entry to be created, potentially overflowing the vector capacity. +// let mut series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); + +// // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity +// let period_info = PeriodInfo { +// number: period_2, +// period_type: PeriodType::BuildAndEarn, +// ending_era: 51, +// }; +// let unstake_amount = 3; +// assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); +// assert_eq!(series.len(), 3); + +// assert_eq!( +// series.get(era_1, period_1), +// None, +// "Oldest entry should have been prunned" +// ); +// assert_eq!( +// series +// .get(era_2, period_2) +// .expect("Entry must exist.") +// .total_staked_amount(), +// stake_amount_2 +// ); +// assert_eq!( +// series +// .get(era_2 + 1, period_2) +// .expect("Entry must exist.") +// .total_staked_amount(), +// stake_amount_2 - unstake_amount +// ); +// assert_eq!( +// series +// .get(era_3, period_2) +// .expect("Entry must exist.") +// .total_staked_amount(), +// stake_amount_3 - unstake_amount +// ); +// } + +// #[test] +// fn contract_staking_info_series_unstake_with_inconsistent_data_fails() { +// let mut series = ContractStakingInfoSeries::default(); +// let era = 5; +// let period = 2; +// let period_info = PeriodInfo { +// number: period, +// period_type: PeriodType::Voting, +// ending_era: 31, +// }; + +// // 1st - Unstake from empty series +// assert!(series.unstake(1, period_info, era).is_err()); + +// // 2nd - Unstake with old period +// let amount = 37; +// assert!(series.stake(amount, period_info, era).is_ok()); + +// let old_period_info = { +// let mut temp = period_info.clone(); +// temp.number -= 1; +// temp +// }; +// assert!(series.unstake(1, old_period_info, era - 1).is_err()); + +// // 3rd - Unstake with 'too' old era +// assert!(series.unstake(1, period_info, era - 2).is_err()); +// assert!(series.unstake(1, period_info, era - 1).is_ok()); +// } + +// #[test] +// fn era_reward_span_push_and_get_works() { +// get_u32_type!(SpanLength, 8); +// let mut era_reward_span = EraRewardSpan::::new(); + +// // Sanity checks +// assert!(era_reward_span.is_empty()); +// assert!(era_reward_span.len().is_zero()); +// assert!(era_reward_span.first_era().is_zero()); +// assert!(era_reward_span.last_era().is_zero()); + +// // Insert some values and verify state change +// let era_1 = 5; +// let era_reward_1 = EraReward::new(23, 41); +// assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); +// assert_eq!(era_reward_span.len(), 1); +// assert_eq!(era_reward_span.first_era(), era_1); +// assert_eq!(era_reward_span.last_era(), era_1); + +// // Insert another value and verify state change +// let era_2 = era_1 + 1; +// let era_reward_2 = EraReward::new(37, 53); +// assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); +// assert_eq!(era_reward_span.len(), 2); +// assert_eq!(era_reward_span.first_era(), era_1); +// assert_eq!(era_reward_span.last_era(), era_2); + +// // Get the values and verify they are as expected +// assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); +// assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); +// } + +// #[test] +// fn era_reward_span_fails_when_expected() { +// // Capacity is only 2 to make testing easier +// get_u32_type!(SpanLength, 2); +// let mut era_reward_span = EraRewardSpan::::new(); + +// // Push first values to get started +// let era_1 = 5; +// let era_reward = EraReward::new(23, 41); +// assert!(era_reward_span.push(era_1, era_reward).is_ok()); + +// // Attempting to push incorrect era results in an error +// for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { +// assert_eq!( +// era_reward_span.push(*wrong_era, era_reward), +// Err(EraRewardSpanError::InvalidEra) +// ); +// } + +// // Pushing above capacity results in an error +// let era_2 = era_1 + 1; +// assert!(era_reward_span.push(era_2, era_reward).is_ok()); +// let era_3 = era_2 + 1; +// assert_eq!( +// era_reward_span.push(era_3, era_reward), +// Err(EraRewardSpanError::NoCapacity) +// ); +// } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 267185bd9f..1d15e0e548 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -26,16 +26,16 @@ use sp_runtime::{ use astar_primitives::Balance; -use crate::pallet::Config; +// use crate::pallet::Config; // TODO: instead of using `pub` visiblity for fields, either use `pub(crate)` or add dedicated methods for accessing them. /// Convenience type for `AccountLedger` usage. -pub type AccountLedgerFor = AccountLedger< - BlockNumberFor, - ::MaxUnlockingChunks, - ::MaxStakingChunks, ->; +// pub type AccountLedgerFor = AccountLedger< +// BlockNumberFor, +// ::MaxUnlockingChunks, +// ::MaxStakingChunks, +// >; /// Era number type pub type EraNumber = u32; @@ -44,27 +44,11 @@ pub type PeriodNumber = u32; /// Dapp Id type pub type DAppId = u16; -// TODO: perhaps this trait is not needed and instead of having 2 separate '___Chunk' types, we can have just one? -/// Trait for types that can be used as a pair of amount & era. -pub trait AmountEraPair: MaxEncodedLen + Default + Copy { - fn new(amount: Balance, era: EraNumber) -> Self; - /// Balance amount used somehow during the accompanied era. - fn get_amount(&self) -> Balance; - /// Era acting as timestamp for the accompanied amount. - fn get_era(&self) -> EraNumber; - // Sets the era to the specified value. - fn set_era(&mut self, era: EraNumber); - /// Increase the total amount by the specified increase, saturating at the maximum value. - fn saturating_accrue(&mut self, increase: Balance); - /// Reduce the total amount by the specified reduction, saturating at the minumum value. - fn saturating_reduce(&mut self, reduction: Balance); -} - /// Simple enum representing errors possible when using sparse bounded vector. #[derive(Debug, PartialEq, Eq)] pub enum AccountLedgerError { - /// Old era values cannot be added. - OldEra, + /// Old or future era values cannot be added. + InvalidEra, /// Bounded storage capacity exceeded. NoCapacity, /// Invalid period specified. @@ -73,237 +57,8 @@ pub enum AccountLedgerError { UnavailableStakeFunds, /// Unstake amount is to large in respect to what's staked. UnstakeAmountLargerThanStake, - /// Split era is invalid; it's not contained withing the vector's scope. - SplitEraInvalid, -} - -/// Helper struct for easier manipulation of sparse pairs. -/// -/// The struct guarantees the following: -/// ----------------------------------- -/// 1. The vector is always sorted by era, in ascending order. -/// 2. There are no two consecutive zero chunks. -/// 3. There are no two chunks with the same era. -/// 4. The vector is always bounded by the specified maximum length. -/// -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] -#[scale_info(skip_type_params(ML))] -pub struct SparseBoundedAmountEraVec>(pub BoundedVec); - -impl SparseBoundedAmountEraVec -where - P: AmountEraPair, - ML: Get, -{ - /// Create new instance - pub fn new() -> Self { - Self(BoundedVec::::default()) - } - - /// Places the specified pair into the vector, in an appropriate place. - /// - /// There are two possible successful scenarios: - /// 1. If entry for the specified era already exists, it's updated. - /// [(100, 1)] -- add_amount(50, 1) --> [(150, 1)] - /// - /// 2. If entry for the specified era doesn't exist, it's created and insertion is attempted. - /// [(100, 1)] -- add_amount(50, 2) --> [(100, 1), (150, 2)] - /// - /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn add_amount( - &mut self, - amount: Balance, - era: EraNumber, - ) -> Result<(), AccountLedgerError> { - if amount.is_zero() { - return Ok(()); - } - - let mut chunk = if let Some(&chunk) = self.0.last() { - ensure!(chunk.get_era() <= era, AccountLedgerError::OldEra); - chunk - } else { - P::default() - }; - - chunk.saturating_accrue(amount); - - if chunk.get_era() == era && !self.0.is_empty() { - if let Some(last) = self.0.last_mut() { - *last = chunk; - } - } else { - chunk.set_era(era); - self.0 - .try_push(chunk) - .map_err(|_| AccountLedgerError::NoCapacity)?; - } - - Ok(()) - } - - /// Subtracts the specified amount of the total locked amount, if possible. - /// - /// There are multiple success scenarios/rules: - /// 1. If entry for the specified era already exists, it's updated. - /// a. [(100, 1)] -- subtract_amount(50, 1) --> [(50, 1)] - /// b. [(100, 1)] -- subtract_amount(100, 1) --> [] - /// - /// 2. All entries following the specified era will have their amount reduced as well. - /// [(100, 1), (150, 2)] -- subtract_amount(50, 1) --> [(50, 1), (100, 2)] - /// - /// 3. If entry for the specified era doesn't exist, it's created and insertion is attempted. - /// [(100, 1), (200, 3)] -- subtract_amount(100, 2) --> [(100, 1), (0, 2), (100, 3)] - /// - /// 4. No two consecutive zero chunks are allowed. - /// [(100, 1), (0, 2), (100, 3), (200, 4)] -- subtract_amount(100, 3) --> [(100, 1), (0, 2), (100, 4)] - /// - /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn subtract_amount( - &mut self, - amount: Balance, - era: EraNumber, - ) -> Result<(), AccountLedgerError> { - if amount.is_zero() || self.0.is_empty() { - return Ok(()); - } - // TODO: this method can surely be optimized (avoid too many iters) but focus on that later, - // when it's all working fine, and we have good test coverage. - // TODO2: realistically, the only eligible eras are the last two ones (current & previous). Code could be optimized for that. - - // Find the most relevant locked chunk for the specified era - let index = if let Some(index) = self.0.iter().rposition(|&chunk| chunk.get_era() <= era) { - index - } else { - // Covers scenario when there's only 1 chunk for the next era, and remove it if it's zero. - self.0 - .iter_mut() - .for_each(|chunk| chunk.saturating_reduce(amount)); - self.0.retain(|chunk| !chunk.get_amount().is_zero()); - return Ok(()); - }; - - // Update existing or insert a new chunk - let mut inner = self.0.clone().into_inner(); - let relevant_chunk_index = if inner[index].get_era() == era { - inner[index].saturating_reduce(amount); - index - } else { - // Take the most relevant chunk for the desired era, - // and use it as 'base' for the new chunk. - let mut chunk = inner[index]; - chunk.saturating_reduce(amount); - chunk.set_era(era); - - // Insert the new chunk AFTER the previous 'most relevant chunk'. - // The chunk we find is always either for the requested era, or some era before it. - inner.insert(index + 1, chunk); - index + 1 - }; - - // Update all chunks after the relevant one, and remove eligible zero chunks - inner[relevant_chunk_index + 1..] - .iter_mut() - .for_each(|chunk| chunk.saturating_reduce(amount)); - - // Prune all consecutive zero chunks - let mut new_inner = Vec::

::new(); - new_inner.push(inner[0]); - for i in 1..inner.len() { - if inner[i].get_amount().is_zero() && inner[i - 1].get_amount().is_zero() { - continue; - } else { - new_inner.push(inner[i]); - } - } - - inner = new_inner; - - // Cleanup if only one zero chunk exists - if inner.len() == 1 && inner[0].get_amount().is_zero() { - inner.pop(); - } - - // Update `locked` to the new vector - self.0 = BoundedVec::try_from(inner).map_err(|_| AccountLedgerError::NoCapacity)?; - - Ok(()) - } - - /// Splits the vector into two parts, using the provided `era` as the splitting point. - /// - /// All entries which satisfy the condition `entry.era <= era` are removed from the vector. - /// After split is done, the _removed_ part is padded with an entry, IFF the last element doesn't cover the specified era. - /// The same is true for the remaining part of the vector. - /// - /// The `era` argument **must** be contained within the vector's scope. - /// - /// E.g.: - /// a) [1,2,6,7] -- split(4) --> [1,2],[5,6,7] - /// b) [1,2] -- split(4) --> [1,2],[5] - /// c) [1,2,4,5] -- split(4) --> [1,2,4],[5] - /// d) [1,2,4,6] -- split(4) --> [1,2,4],[5,6] - /// e) [1,2(0)] -- split(4) --> [1,2],[] // 2nd entry has 'zero' amount, so no need to keep it - pub fn left_split(&mut self, era: EraNumber) -> Result { - // Split the inner vector into two parts - let (left, mut right): (Vec

, Vec

) = self - .0 - .clone() - .into_iter() - .partition(|chunk| chunk.get_era() <= era); - - if left.is_empty() { - return Err(AccountLedgerError::SplitEraInvalid); - } - - if let Some(&last_l_chunk) = left.last() { - // In case 'right' part is missing an entry covering the specified era, add it. - let maybe_chunk = match right.first() { - Some(first_r_chunk) if first_r_chunk.get_era() > era.saturating_add(1) => { - let mut new_chunk = last_l_chunk.clone(); - new_chunk.set_era(era.saturating_add(1)); - Some(new_chunk) - } - None => { - let mut new_chunk = last_l_chunk.clone(); - new_chunk.set_era(era.saturating_add(1)); - Some(new_chunk) - } - _ => None, - }; - - // Only insert the chunk if it's non-zero - match maybe_chunk { - Some(chunk) if chunk.get_amount() > Balance::zero() => { - right.insert(0, chunk); - } - _ => (), - } - } - - // TODO: I could just use `expect` here since it's impossible for vectors to increase in size. - self.0 = BoundedVec::try_from(right).map_err(|_| AccountLedgerError::NoCapacity)?; - - Ok(Self( - BoundedVec::::try_from(left).map_err(|_| AccountLedgerError::NoCapacity)?, - )) - } - - /// Returns the most appropriate chunk for the specified era, if it exists. - pub fn get(&self, era: EraNumber) -> Option

{ - match self.0.binary_search_by(|chunk| chunk.get_era().cmp(&era)) { - // Entry exists, all good. - Ok(idx) => self.0.get(idx).map(|x| *x), - // Entry doesn't exist, but another one covers it so we return it. - Err(idx) if idx > 0 => self.0.get(idx.saturating_sub(1)).map(|x| { - let mut new_chunk = *x; - new_chunk.set_era(era); - new_chunk - }), - // Era is out of scope. - _ => None, - } - } + /// Nothing to claim. + NothingToClaim, } // TODO: rename to SubperiodType? It would be less ambigious. @@ -433,6 +188,7 @@ where self.next_era_start <= now } + // TODO: rename this into something better? /// Triggers the next period type, updating appropriate parameters. pub fn next_period_type(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { let period_number = if self.period_type() == PeriodType::BuildAndEarn { @@ -494,89 +250,56 @@ where } } -/// Information about how much was staked in a specific era. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct StakeChunk { - #[codec(compact)] - pub amount: Balance, - #[codec(compact)] - pub era: EraNumber, -} - -impl Default for StakeChunk { - fn default() -> Self { - Self { - amount: Balance::zero(), - era: EraNumber::zero(), - } - } -} - -impl AmountEraPair for StakeChunk { - fn new(amount: Balance, era: EraNumber) -> Self { - Self { amount, era } - } - fn get_amount(&self) -> Balance { - self.amount - } - fn get_era(&self) -> EraNumber { - self.era - } - fn set_era(&mut self, era: EraNumber) { - self.era = era; - } - fn saturating_accrue(&mut self, increase: Balance) { - self.amount.saturating_accrue(increase); - } - fn saturating_reduce(&mut self, reduction: Balance) { - self.amount.saturating_reduce(reduction); - } -} - /// General info about user's stakes #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] -#[scale_info(skip_type_params(UnlockingLen, StakedLen))] +#[scale_info(skip_type_params(UnlockingLen))] pub struct AccountLedger< BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, UnlockingLen: Get, - StakedLen: Get, > { /// How much active locked amount an account has. pub locked: Balance, /// How much started unlocking on a certain block pub unlocking: BoundedVec, UnlockingLen>, - /// How much user had staked in some period - pub staked: SparseBoundedAmountEraVec, - /// Last period in which account had staked. - pub staked_period: Option, + /// How much user has/had staked in a particular era. + pub staked: StakeAmount, + /// Helper staked amount to keep track of future era stakes. + /// Both `stake` and `staked_future` must ALWAYS refer to the same period. + pub staked_future: Option, + /// TODO + pub staker_rewards_claimed: bool, + /// TODO + pub bonus_reward_claimed: bool, } -impl Default - for AccountLedger +impl Default for AccountLedger where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, UnlockingLen: Get, - StakedLen: Get, { fn default() -> Self { Self { locked: Balance::zero(), unlocking: BoundedVec::, UnlockingLen>::default(), - staked: SparseBoundedAmountEraVec(BoundedVec::::default()), - staked_period: None, + staked: StakeAmount::default(), + staked_future: None, + staker_rewards_claimed: false, + bonus_reward_claimed: false, } } } -impl AccountLedger +impl AccountLedger where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, UnlockingLen: Get, - StakedLen: Get, { /// Empty if no locked/unlocking/staked info exists. pub fn is_empty(&self) -> bool { - self.locked.is_zero() && self.unlocking.is_empty() && self.staked.0.is_empty() + self.locked.is_zero() + && self.unlocking.is_empty() + && self.staked.total().is_zero() + && self.staked_future.is_none() } /// Returns active locked amount. @@ -652,7 +375,7 @@ where /// Amount available for unlocking. pub fn unlockable_amount(&self, current_period: PeriodNumber) -> Balance { self.active_locked_amount() - .saturating_sub(self.active_stake(current_period)) + .saturating_sub(self.staked_amount(current_period)) } /// Claims all of the fully unlocked chunks, and returns the total claimable amount. @@ -681,42 +404,46 @@ where amount } - /// Active staked balance. - /// - /// In case latest stored information is from the past period, active stake is considered to be zero. - pub fn active_stake(&self, active_period: PeriodNumber) -> Balance { - match self.staked_period { - Some(last_staked_period) if last_staked_period == active_period => self - .staked - .0 - .last() - .map_or(Balance::zero(), |chunk| chunk.amount), - _ => Balance::zero(), - } - } - /// Amount that is available for staking. /// /// This is equal to the total active locked amount, minus the staked amount already active. pub fn stakeable_amount(&self, active_period: PeriodNumber) -> Balance { self.active_locked_amount() - .saturating_sub(self.active_stake(active_period)) + .saturating_sub(self.staked_amount(active_period)) } /// Amount that is staked, in respect to currently active period. pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance { - match self.staked_period { - Some(last_staked_period) if last_staked_period == active_period => self - .staked - .0 - .last() - // We should never fallback to the default value since that would mean ledger is in invalid state. - // TODO: perhaps this can be implemented in a better way to have some error handling? Returning 0 might not be the most secure way to handle it. - .map_or(Balance::zero(), |chunk| chunk.amount), - _ => Balance::zero(), + // First check the 'future' entry, afterwards check the 'first' entry + match self.staked_future { + Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(), + _ => match self.staked { + stake_amount if stake_amount.period == active_period => stake_amount.total(), + _ => Balance::zero(), + }, } } + pub fn staked_amount_for_type( + &self, + period_type: PeriodType, + active_period: PeriodNumber, + ) -> Balance { + // First check the 'future' entry, afterwards check the 'first' entry + match self.staked_future { + Some(stake_amount) if stake_amount.period == active_period => { + stake_amount.for_type(period_type) + } + _ => match self.staked { + stake_amount if stake_amount.period == active_period => { + stake_amount.for_type(period_type) + } + _ => Balance::zero(), + }, + } + } + + // TODO: update this /// Adds the specified amount to total staked amount, if possible. /// /// Staking is only allowed if one of the two following conditions is met: @@ -728,25 +455,39 @@ where &mut self, amount: Balance, era: EraNumber, - current_period: PeriodNumber, + current_period_info: PeriodInfo, ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } - match self.staked_period { - Some(last_staked_period) if last_staked_period != current_period => { + // TODO: maybe the check can be nicer? + if self.staked.era > EraNumber::zero() { + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { return Err(AccountLedgerError::InvalidPeriod); } - _ => (), } - if self.stakeable_amount(current_period) < amount { + if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); } - self.staked.add_amount(amount, era)?; - self.staked_period = Some(current_period); + // Update existing entry if it exists, otherwise create it. + match self.staked_future.as_mut() { + Some(stake_amount) => { + stake_amount.add(amount, current_period_info.period_type); + } + None => { + let mut stake_amount = self.staked; + stake_amount.era = era + 1; + stake_amount.period = current_period_info.number; + stake_amount.add(amount, current_period_info.period_type); + self.staked_future = Some(stake_amount); + } + } Ok(()) } @@ -759,51 +500,31 @@ where &mut self, amount: Balance, era: EraNumber, - current_period: PeriodNumber, + current_period_info: PeriodInfo, ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } - // Cannot unstake if the period has passed. - match self.staked_period { - Some(last_staked_period) if last_staked_period != current_period => { - return Err(AccountLedgerError::InvalidPeriod); - } - _ => (), + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); } // User must be precise with their unstake amount. - if self.staked_amount(current_period) < amount { + if self.staked_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnstakeAmountLargerThanStake); } - self.staked.subtract_amount(amount, era) - } - - // TODO: remove this - /// Last era for which a stake entry exists. - /// If no stake entries exist, returns `None`. - pub fn last_stake_era(&self) -> Option { - self.staked.0.last().map(|chunk| chunk.era) - } - - // TODO: remove this - /// First entry for which a stake entry exists. - /// If no stake entries exist, returns `None`. - pub fn oldest_stake_era(&self) -> Option { - self.staked.0.first().map(|chunk| chunk.era) - } - - // TODO - pub fn first_and_last_stake_chunks(&self) -> Option<(StakeChunk, StakeChunk)> { - let first = self.staked.0.first().map(|chunk| *chunk); - let last = self.staked.0.last().map(|chunk| *chunk); - - match (first, last) { - (Some(first), Some(last)) => Some((first, last)), - _ => None, + self.staked + .subtract(amount, current_period_info.period_type); + if let Some(stake_amount) = self.staked_future.as_mut() { + stake_amount.subtract(amount, current_period_info.period_type); } + + Ok(()) } /// Claim up stake chunks up to the specified `era`. @@ -814,54 +535,68 @@ where &mut self, era: EraNumber, period_end: Option, - ) -> Result, AccountLedgerError> { - let claim_chunks = self.staked.left_split(era)?; - - // Check if all possible chunks have been claimed - match self.staked.0.first() { - Some(chunk) => { - match period_end { - // If first chunk is after the period end, clearly everything has been claimed - Some(period_end) if chunk.get_era() > period_end => { - self.staked_period = None; - self.staked = SparseBoundedAmountEraVec::new(); - } - _ => (), - } - } - // No more chunks remain, meaning everything has been claimed - None => { - self.staked_period = None; - } + // TODO: add a better type later + ) -> Result<(EraNumber, EraNumber, Balance), AccountLedgerError> { + if era <= self.staked.era || self.staked.total().is_zero() { + return Err(AccountLedgerError::NothingToClaim); } - // TODO: this is a bit clunky - introduce variables instead that keep track whether rewards were claimed or not. + let result = (self.staked.era, era, self.staked.total()); + + // Update latest 'staked' era + self.staked.era = era; - Ok(claim_chunks) + // Make sure to clean + match period_end { + Some(ending_era) if era >= ending_era => { + self.staker_rewards_claimed = true; + self.staked = Default::default(); + self.staked_future = None; + } + _ => (), + } + + Ok(result) } } -// TODO: it would be nice to implement add/subtract logic on this struct and use it everywhere -// we need to keep track of staking amount for periods. Right now I have logic duplication which is not good. +// TODO #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct StakeAmount { /// Amount of staked funds accounting for the voting period. #[codec(compact)] - voting: Balance, + pub voting: Balance, /// Amount of staked funds accounting for the build&earn period. #[codec(compact)] - build_and_earn: Balance, + pub build_and_earn: Balance, + /// Era to which this stake amount refers to. + #[codec(compact)] + pub era: EraNumber, + /// Period to which this stake amount refers to. + #[codec(compact)] + pub period: PeriodNumber, } impl StakeAmount { /// Create new instance of `StakeAmount` with specified `voting` and `build_and_earn` amounts. - pub fn new(voting: Balance, build_and_earn: Balance) -> Self { + pub fn new( + voting: Balance, + build_and_earn: Balance, + era: EraNumber, + period: PeriodNumber, + ) -> Self { Self { voting, build_and_earn, + era, + period, } } + pub fn is_empty(&self) -> bool { + self.voting.is_zero() && self.build_and_earn.is_zero() + } + /// Total amount staked in both period types. pub fn total(&self) -> Balance { self.voting.saturating_add(self.build_and_earn) @@ -875,23 +610,21 @@ impl StakeAmount { } } - // TODO: rename to add? /// Stake the specified `amount` for the specified `period_type`. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { + pub fn add(&mut self, amount: Balance, period_type: PeriodType) { match period_type { PeriodType::Voting => self.voting.saturating_accrue(amount), PeriodType::BuildAndEarn => self.build_and_earn.saturating_accrue(amount), } } - // TODO: rename to subtract? /// Unstake the specified `amount` for the specified `period_type`. /// /// In case period type is `Voting`, the amount is subtracted from the voting period. /// /// In case period type is `Build&Earn`, the amount is first subtracted from the /// build&earn amount, and any rollover is subtracted from the voting period. - pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) { + pub fn subtract(&mut self, amount: Balance, period_type: PeriodType) { match period_type { PeriodType::Voting => self.voting.saturating_reduce(amount), PeriodType::BuildAndEarn => { @@ -949,13 +682,13 @@ impl EraInfo { /// Add the specified `amount` to the appropriate stake amount, based on the `PeriodType`. pub fn add_stake_amount(&mut self, amount: Balance, period_type: PeriodType) { - self.next_stake_amount.stake(amount, period_type); + self.next_stake_amount.add(amount, period_type); } /// Subtract the specified `amount` from the appropriate stake amount, based on the `PeriodType`. pub fn unstake_amount(&mut self, amount: Balance, period_type: PeriodType) { - self.current_stake_amount.unstake(amount, period_type); - self.next_stake_amount.unstake(amount, period_type); + self.current_stake_amount.subtract(amount, period_type); + self.next_stake_amount.subtract(amount, period_type); } /// Total staked amount in this era. @@ -1002,15 +735,8 @@ impl EraInfo { /// Keeps track of amount staked in the 'voting period', as well as 'build&earn period'. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct SingularStakingInfo { - /// Total amount staked during the voting period. - #[codec(compact)] - vp_staked_amount: Balance, - /// Total amount staked during the build&earn period. - #[codec(compact)] - bep_staked_amount: Balance, - /// Period number for which this entry is relevant. - #[codec(compact)] - period: PeriodNumber, + /// Staked amount + staked: StakeAmount, /// Indicates whether a staker is a loyal staker or not. loyal_staker: bool, } @@ -1024,9 +750,8 @@ impl SingularStakingInfo { /// `period_type` - period type during which this entry is created. pub fn new(period: PeriodNumber, period_type: PeriodType) -> Self { Self { - vp_staked_amount: Balance::zero(), - bep_staked_amount: Balance::zero(), - period, + // TODO: one drawback here is using the struct which has `era` as the field - it's not needed here. Should I add a special struct just for this? + staked: StakeAmount::new(Balance::zero(), Balance::zero(), 0, period), // Loyalty staking is only possible if stake is first made during the voting period. loyal_staker: period_type == PeriodType::Voting, } @@ -1034,10 +759,7 @@ impl SingularStakingInfo { /// Stake the specified amount on the contract, for the specified period type. pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.vp_staked_amount.saturating_accrue(amount), - PeriodType::BuildAndEarn => self.bep_staked_amount.saturating_accrue(amount), - } + self.staked.add(amount, period_type); } /// Unstakes some of the specified amount from the contract. @@ -1047,43 +769,32 @@ impl SingularStakingInfo { /// /// Returns the amount that was unstaked from the `voting period` stake, and from the `build&earn period` stake. pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) -> (Balance, Balance) { - // If B&E period stake can cover the unstaking amount, just reduce it. - if self.bep_staked_amount >= amount { - self.bep_staked_amount.saturating_reduce(amount); - (Balance::zero(), amount) - } else { - // In case we have to dip into the voting period stake, make sure B&E period stake is reduced first. - // Also make sure to remove loyalty flag from the staker. - let vp_staked_amount_snapshot = self.vp_staked_amount; - let bep_amount_snapshot = self.bep_staked_amount; - let leftover_amount = amount.saturating_sub(self.bep_staked_amount); - - self.vp_staked_amount.saturating_reduce(leftover_amount); - self.bep_staked_amount = Balance::zero(); - - // It's ok if staker reduces their stake amount during voting period. - // Once loyalty flag is removed, it cannot be returned. - self.loyal_staker = self.loyal_staker && period_type == PeriodType::Voting; - - // Actual amount that was unstaked: (voting period unstake, B&E period unstake) - ( - vp_staked_amount_snapshot.saturating_sub(self.vp_staked_amount), - bep_amount_snapshot, - ) - } + let snapshot = self.staked; + + self.staked.subtract(amount, period_type); + + self.loyal_staker = self.loyal_staker + && (period_type == PeriodType::Voting + || period_type == PeriodType::BuildAndEarn + && self.staked.voting == snapshot.voting); + + // Amount that was unstaked + ( + snapshot.voting.saturating_sub(self.staked.voting), + snapshot + .build_and_earn + .saturating_sub(self.staked.build_and_earn), + ) } /// Total staked on the contract by the user. Both period type stakes are included. pub fn total_staked_amount(&self) -> Balance { - self.vp_staked_amount.saturating_add(self.bep_staked_amount) + self.staked.total() } /// Returns amount staked in the specified period. pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - match period_type { - PeriodType::Voting => self.vp_staked_amount, - PeriodType::BuildAndEarn => self.bep_staked_amount, - } + self.staked.for_type(period_type) } /// If `true` staker has staked during voting period and has never reduced their sta @@ -1093,94 +804,12 @@ impl SingularStakingInfo { /// Period for which this entry is relevant. pub fn period_number(&self) -> PeriodNumber { - self.period + self.staked.period } /// `true` if no stake exists, `false` otherwise. pub fn is_empty(&self) -> bool { - self.vp_staked_amount.is_zero() && self.bep_staked_amount.is_zero() - } -} - -/// Information about how much was staked on a contract during a specific era or period. -/// -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] -pub struct ContractStakingInfo { - #[codec(compact)] - vp_staked_amount: Balance, - #[codec(compact)] - bep_staked_amount: Balance, - #[codec(compact)] - era: EraNumber, - #[codec(compact)] - period: PeriodNumber, -} - -impl ContractStakingInfo { - /// Create new instance of `ContractStakingInfo` with specified era & period. - /// These parameters are immutable. - /// - /// Staked amounts are initialized to zero and can be increased or decreased. - pub fn new(era: EraNumber, period: PeriodNumber) -> Self { - Self { - vp_staked_amount: Balance::zero(), - bep_staked_amount: Balance::zero(), - era, - period, - } - } - - /// Total staked amount on the contract. - pub fn total_staked_amount(&self) -> Balance { - self.vp_staked_amount.saturating_add(self.bep_staked_amount) - } - - /// Staked amount of the specified period type. - /// - /// Note: - /// It is possible that voting period stake is reduced during the build&earn period. - /// This is because stakers can unstake their funds during the build&earn period, which can - /// chip away from the voting period stake. - pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - match period_type { - PeriodType::Voting => self.vp_staked_amount, - PeriodType::BuildAndEarn => self.bep_staked_amount, - } - } - - /// Era for which this entry is relevant. - pub fn era(&self) -> EraNumber { - self.era - } - - /// Period for which this entry is relevant. - pub fn period(&self) -> PeriodNumber { - self.period - } - - /// Stake specified `amount` on the contract, for the specified `period_type`. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.vp_staked_amount.saturating_accrue(amount), - PeriodType::BuildAndEarn => self.bep_staked_amount.saturating_accrue(amount), - } - } - - /// Unstake specified `amount` from the contract, for the specified `period_type`. - pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.vp_staked_amount.saturating_reduce(amount), - PeriodType::BuildAndEarn => { - let overflow = amount.saturating_sub(self.bep_staked_amount); - self.bep_staked_amount.saturating_reduce(amount); - self.vp_staked_amount.saturating_reduce(overflow); - } - } - } - - /// `true` if no stake exists, `false` otherwise. - pub fn is_empty(&self) -> bool { - self.vp_staked_amount.is_zero() && self.bep_staked_amount.is_zero() + self.staked.is_empty() } } @@ -1188,19 +817,17 @@ const STAKING_SERIES_HISTORY: u32 = 3; /// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct ContractStakingInfoSeries( - BoundedVec>, -); -impl ContractStakingInfoSeries { - /// Helper function to create a new instance of `ContractStakingInfoSeries`. +pub struct StakeAmountSeries(BoundedVec>); +impl StakeAmountSeries { + /// Helper function to create a new instance of `StakeAmountSeries`. #[cfg(test)] - pub fn new(inner: Vec) -> Self { + pub fn new(inner: Vec) -> Self { Self(BoundedVec::try_from(inner).expect("Test should ensure this is always valid")) } - /// Returns inner `Vec` of `ContractStakingInfo` instances. Useful for testing. + /// Returns inner `Vec` of `StakeAmount` instances. Useful for testing. #[cfg(test)] - pub fn inner(&self) -> Vec { + pub fn inner(&self) -> Vec { self.0.clone().into_inner() } @@ -1214,9 +841,11 @@ impl ContractStakingInfoSeries { self.0.is_empty() } - /// Returns the `ContractStakingInfo` type for the specified era & period, if it exists. - pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { - let idx = self.0.binary_search_by(|info| info.era().cmp(&era)); + /// Returns the `StakeAmount` type for the specified era & period, if it exists. + pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { + let idx = self + .0 + .binary_search_by(|stake_amount| stake_amount.era.cmp(&era)); // There are couple of distinct scenarios: // 1. Era exists, so we just return it. @@ -1231,7 +860,7 @@ impl ContractStakingInfoSeries { None } else { match self.0.get(ideal_idx - 1) { - Some(info) if info.period() == period => { + Some(info) if info.period == period => { let mut info = *info; info.era = era; Some(info) @@ -1245,20 +874,18 @@ impl ContractStakingInfoSeries { /// Last era for which a stake entry exists, `None` if no entries exist. pub fn last_stake_era(&self) -> Option { - self.0.last().map(|info| info.era()) + self.0.last().map(|info| info.era) } /// Last period for which a stake entry exists, `None` if no entries exist. pub fn last_stake_period(&self) -> Option { - self.0.last().map(|info| info.period()) + self.0.last().map(|info| info.period) } /// Total staked amount on the contract, in the active period. pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { match self.0.last() { - Some(last_element) if last_element.period() == active_period => { - last_element.total_staked_amount() - } + Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(), _ => Balance::zero(), } } @@ -1266,8 +893,8 @@ impl ContractStakingInfoSeries { /// Staked amount on the contract, for specified period type, in the active period. pub fn staked_amount(&self, period: PeriodNumber, period_type: PeriodType) -> Balance { match self.0.last() { - Some(last_element) if last_element.period() == period => { - last_element.staked_amount(period_type) + Some(stake_amount) if stake_amount.period == period => { + stake_amount.for_type(period_type) } _ => Balance::zero(), } @@ -1281,41 +908,39 @@ impl ContractStakingInfoSeries { era: EraNumber, ) -> Result<(), ()> { // Defensive check to ensure we don't end up in a corrupted state. Should never happen. - if let Some(last_element) = self.0.last() { - if last_element.era() > era || last_element.period() > period_info.number { + if let Some(stake_amount) = self.0.last() { + if stake_amount.era > era || stake_amount.period > period_info.number { return Err(()); } } - // Get the most relevant `ContractStakingInfo` instance - let mut staking_info = if let Some(last_element) = self.0.last() { - if last_element.era() == era { + // Get the most relevant `StakeAmount` instance + let mut stake_amount = if let Some(stake_amount) = self.0.last() { + if stake_amount.era == era { // Era matches, so we just update the last element. - let last_element = *last_element; + let stake_amount = *stake_amount; let _ = self.0.pop(); - last_element - } else if last_element.period() == period_info.number { + stake_amount + } else if stake_amount.period == period_info.number { // Periods match so we should 'copy' the last element to get correct staking amount - let mut temp = *last_element; + let mut temp = *stake_amount; temp.era = era; temp } else { // It's a new period, so we need a completely new instance - ContractStakingInfo::new(era, period_info.number) + StakeAmount::new(Balance::zero(), Balance::zero(), era, period_info.number) } } else { // It's a new period, so we need a completely new instance - ContractStakingInfo::new(era, period_info.number) + StakeAmount::new(Balance::zero(), Balance::zero(), era, period_info.number) }; // Update the stake amount - staking_info.stake(amount, period_info.period_type); - - // Prune before pushing the new entry - self.prune(); + stake_amount.add(amount, period_info.period_type); // This should be infalible due to previous checks that ensure we don't end up overflowing the vector. - self.0.try_push(staking_info).map_err(|_| ()) + self.prune(); + self.0.try_push(stake_amount).map_err(|_| ()) } /// Unstake the specified `amount` from the contract, for the specified `period_type` and `era`. @@ -1325,11 +950,12 @@ impl ContractStakingInfoSeries { period_info: PeriodInfo, era: EraNumber, ) -> Result<(), ()> { + // TODO: look into refactoring/optimizing this - right now it's a bit complex. + // Defensive check to ensure we don't end up in a corrupted state. Should never happen. - if let Some(last_element) = self.0.last() { + if let Some(stake_amount) = self.0.last() { // It's possible last element refers to the upcoming era, hence the "-1" on the 'era'. - if last_element.era().saturating_sub(1) > era - || last_element.period() > period_info.number + if stake_amount.era.saturating_sub(1) > era || stake_amount.period > period_info.number { return Err(()); } @@ -1341,11 +967,11 @@ impl ContractStakingInfoSeries { // 1st step - remove the last element IFF it's for the next era. // Unstake the requested amount from it. let last_era_info = match self.0.last() { - Some(last_element) if last_element.era() == era.saturating_add(1) => { - let mut last_element = *last_element; - last_element.unstake(amount, period_info.period_type); + Some(stake_amount) if stake_amount.era == era.saturating_add(1) => { + let mut stake_amount = *stake_amount; + stake_amount.subtract(amount, period_info.period_type); let _ = self.0.pop(); - Some(last_element) + Some(stake_amount) } _ => None, }; @@ -1354,13 +980,13 @@ impl ContractStakingInfoSeries { // 1. - last element has a matching era so we just update it. // 2. - last element has a past era and matching period, so we'll create a new entry based on it. // 3. - last element has a past era and past period, meaning it's invalid. - let second_last_era_info = if let Some(last_element) = self.0.last_mut() { - if last_element.era() == era { - last_element.unstake(amount, period_info.period_type); + let second_last_era_info = if let Some(stake_amount) = self.0.last_mut() { + if stake_amount.era == era { + stake_amount.subtract(amount, period_info.period_type); None - } else if last_element.period() == period_info.number { - let mut new_entry = *last_element; - new_entry.unstake(amount, period_info.period_type); + } else if stake_amount.period == period_info.number { + let mut new_entry = *stake_amount; + new_entry.subtract(amount, period_info.period_type); new_entry.era = era; Some(new_entry) } else { From 39c6cf06cce48037f446667ae6e8a1cdd1aea036 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 16 Oct 2023 19:33:11 +0200 Subject: [PATCH 08/86] Refactoring continued --- pallets/dapp-staking-v3/src/lib.rs | 2204 ++++++++-------- pallets/dapp-staking-v3/src/test/mock.rs | 423 ++-- .../dapp-staking-v3/src/test/tests_types.rs | 2232 ++++++++--------- pallets/dapp-staking-v3/src/types.rs | 78 +- 4 files changed, 2451 insertions(+), 2486 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 4baa8e6b15..8df02a4f90 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -53,7 +53,7 @@ use sp_runtime::{ use astar_primitives::Balance; use crate::types::*; -// pub use pallet::*; +pub use pallet::*; #[cfg(test)] mod test; @@ -62,1109 +62,1099 @@ mod types; const STAKING_ID: LockIdentifier = *b"dapstake"; -// #[frame_support::pallet] -// pub mod pallet { -// use super::*; - -// /// The current storage version. -// const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); - -// #[pallet::pallet] -// #[pallet::storage_version(STORAGE_VERSION)] -// pub struct Pallet(_); - -// #[pallet::config] -// pub trait Config: frame_system::Config { -// /// The overarching event type. -// type RuntimeEvent: From> + IsType<::RuntimeEvent>; - -// /// Currency used for staking. -// /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) -// type Currency: LockableCurrency< -// Self::AccountId, -// Moment = Self::BlockNumber, -// Balance = Balance, -// >; - -// /// Describes smart contract in the context required by dApp staking. -// type SmartContract: Parameter + Member + MaxEncodedLen; - -// /// Privileged origin for managing dApp staking pallet. -// type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; - -// /// Length of a standard era in block numbers. -// #[pallet::constant] -// type StandardEraLength: Get; - -// /// Length of the `Voting` period in standard eras. -// /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras -// /// for the sake of simplicity & consistency. -// #[pallet::constant] -// type StandardErasPerVotingPeriod: Get; - -// /// Length of the `Build&Earn` period in standard eras. -// /// Each `Build&Earn` period consists of one or more distinct standard eras. -// #[pallet::constant] -// type StandardErasPerBuildAndEarnPeriod: Get; - -// /// Maximum length of a single era reward span length entry. -// #[pallet::constant] -// type EraRewardSpanLength: Get; - -// /// Number of periods for which we keep rewards available for claiming. -// /// After that period, they are no longer claimable. -// #[pallet::constant] -// type RewardRetentionInPeriods: Get; - -// /// Maximum number of contracts that can be integrated into dApp staking at once. -// #[pallet::constant] -// type MaxNumberOfContracts: Get; - -// /// Maximum number of unlocking chunks that can exist per account at a time. -// #[pallet::constant] -// type MaxUnlockingChunks: Get; - -// /// Minimum amount an account has to lock in dApp staking in order to participate. -// #[pallet::constant] -// type MinimumLockedAmount: Get; - -// /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. -// #[pallet::constant] -// type UnlockingPeriod: Get>; - -// /// Maximum number of staking chunks that can exist per account at a time. -// #[pallet::constant] -// type MaxStakingChunks: Get; - -// /// Minimum amount staker can stake on a contract. -// #[pallet::constant] -// type MinimumStakeAmount: Get; -// } - -// #[pallet::event] -// #[pallet::generate_deposit(pub(crate) fn deposit_event)] -// pub enum Event { -// /// New era has started. -// NewEra { era: EraNumber }, -// /// New period has started. -// NewPeriod { -// period_type: PeriodType, -// number: PeriodNumber, -// }, -// /// A smart contract has been registered for dApp staking -// DAppRegistered { -// owner: T::AccountId, -// smart_contract: T::SmartContract, -// dapp_id: DAppId, -// }, -// /// dApp reward destination has been updated. -// DAppRewardDestinationUpdated { -// smart_contract: T::SmartContract, -// beneficiary: Option, -// }, -// /// dApp owner has been changed. -// DAppOwnerChanged { -// smart_contract: T::SmartContract, -// new_owner: T::AccountId, -// }, -// /// dApp has been unregistered -// DAppUnregistered { -// smart_contract: T::SmartContract, -// era: EraNumber, -// }, -// /// Account has locked some amount into dApp staking. -// Locked { -// account: T::AccountId, -// amount: Balance, -// }, -// /// Account has started the unlocking process for some amount. -// Unlocking { -// account: T::AccountId, -// amount: Balance, -// }, -// /// Account has claimed unlocked amount, removing the lock from it. -// ClaimedUnlocked { -// account: T::AccountId, -// amount: Balance, -// }, -// /// Account has relocked all of the unlocking chunks. -// Relock { -// account: T::AccountId, -// amount: Balance, -// }, -// /// Account has staked some amount on a smart contract. -// Stake { -// account: T::AccountId, -// smart_contract: T::SmartContract, -// amount: Balance, -// }, -// /// Account has unstaked some amount from a smart contract. -// Unstake { -// account: T::AccountId, -// smart_contract: T::SmartContract, -// amount: Balance, -// }, -// /// Account has claimed some stake rewards. -// Reward { -// account: T::AccountId, -// era: EraNumber, -// amount: Balance, -// }, -// } - -// #[pallet::error] -// pub enum Error { -// /// Pallet is disabled/in maintenance mode. -// Disabled, -// /// Smart contract already exists within dApp staking protocol. -// ContractAlreadyExists, -// /// Maximum number of smart contracts has been reached. -// ExceededMaxNumberOfContracts, -// /// Not possible to assign a new dApp Id. -// /// This should never happen since current type can support up to 65536 - 1 unique dApps. -// NewDAppIdUnavailable, -// /// Specified smart contract does not exist in dApp staking. -// ContractNotFound, -// /// Call origin is not dApp owner. -// OriginNotOwner, -// /// dApp is part of dApp staking but isn't active anymore. -// NotOperatedDApp, -// /// Performing locking or staking with 0 amount. -// ZeroAmount, -// /// Total locked amount for staker is below minimum threshold. -// LockedAmountBelowThreshold, -// /// Cannot add additional locked balance chunks due to capacity limit. -// TooManyLockedBalanceChunks, -// /// Cannot add additional unlocking chunks due to capacity limit. -// TooManyUnlockingChunks, -// /// Remaining stake prevents entire balance of starting the unlocking process. -// RemainingStakePreventsFullUnlock, -// /// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all. -// NoUnlockedChunksToClaim, -// /// There are no unlocking chunks available to relock. -// NoUnlockingChunks, -// /// The amount being staked is too large compared to what's available for staking. -// UnavailableStakeFunds, -// /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. -// UnclaimedRewardsFromPastPeriods, -// /// Cannot add additional stake chunks due to capacity limit. -// TooManyStakeChunks, -// /// An unexpected error occured while trying to stake. -// InternalStakeError, -// /// Total staked amount on contract is below the minimum required value. -// InsufficientStakeAmount, -// /// Stake operation is rejected since period ends in the next era. -// PeriodEndsInNextEra, -// /// Unstaking is rejected since the period in which past stake was active has passed. -// UnstakeFromPastPeriod, -// /// Unstake amount is greater than the staked amount. -// UnstakeAmountTooLarge, -// /// Account has no staking information for the contract. -// NoStakingInfo, -// /// An unexpected error occured while trying to unstake. -// InternalUnstakeError, -// /// Rewards are no longer claimable since they are too old. -// StakerRewardsExpired, -// /// There are no claimable rewards for the account. -// NoClaimableRewards, -// /// An unexpected error occured while trying to claim rewards. -// InternalClaimStakerError, -// } - -// /// General information about dApp staking protocol state. -// #[pallet::storage] -// pub type ActiveProtocolState = -// StorageValue<_, ProtocolState>, ValueQuery>; - -// /// Counter for unique dApp identifiers. -// #[pallet::storage] -// pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; - -// /// Map of all dApps integrated into dApp staking protocol. -// #[pallet::storage] -// pub type IntegratedDApps = CountedStorageMap< -// _, -// Blake2_128Concat, -// T::SmartContract, -// DAppInfo, -// OptionQuery, -// >; - -// /// General locked/staked information for each account. -// #[pallet::storage] -// pub type Ledger = -// StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; - -// /// Information about how much each staker has staked for each smart contract in some period. -// #[pallet::storage] -// pub type StakerInfo = StorageDoubleMap< -// _, -// Blake2_128Concat, -// T::AccountId, -// Blake2_128Concat, -// T::SmartContract, -// SingularStakingInfo, -// OptionQuery, -// >; - -// /// Information about how much has been staked on a smart contract in some era or period. -// #[pallet::storage] -// pub type ContractStake = -// StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakingInfoSeries, ValueQuery>; - -// /// General information about the current era. -// #[pallet::storage] -// pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; - -// /// Information about rewards for each era. -// /// -// /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. -// /// For the sake of simplicity, valid `era` keys are calculated as: -// /// -// /// era_key = era - (era % T::EraRewardSpanLength) -// /// -// /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. -// /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. -// #[pallet::storage] -// pub type EraRewards = -// StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan, OptionQuery>; - -// /// Information about period's end. -// #[pallet::storage] -// pub type PeriodEnd = -// StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; - -// #[pallet::hooks] -// impl Hooks> for Pallet { -// fn on_initialize(now: BlockNumberFor) -> Weight { -// let mut protocol_state = ActiveProtocolState::::get(); - -// // We should not modify pallet storage while in maintenance mode. -// // This is a safety measure, since maintenance mode is expected to be -// // enabled in case some misbehavior or corrupted storage is detected. -// if protocol_state.maintenance { -// return T::DbWeight::get().reads(1); -// } - -// // Nothing to do if it's not new era -// if !protocol_state.is_new_era(now) { -// return T::DbWeight::get().reads(1); -// } - -// let mut era_info = CurrentEraInfo::::get(); - -// let current_era = protocol_state.era; -// let next_era = current_era.saturating_add(1); -// let (maybe_period_event, era_reward) = match protocol_state.period_type() { -// PeriodType::Voting => { -// // For the sake of consistency, we put zero reward into storage -// let era_reward = -// EraReward::new(Balance::zero(), era_info.total_staked_amount()); - -// let ending_era = -// next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); -// let build_and_earn_start_block = -// now.saturating_add(T::StandardEraLength::get()); -// protocol_state.next_period_type(ending_era, build_and_earn_start_block); - -// era_info.migrate_to_next_era(Some(protocol_state.period_type())); - -// ( -// Some(Event::::NewPeriod { -// period_type: protocol_state.period_type(), -// number: protocol_state.period_number(), -// }), -// era_reward, -// ) -// } -// 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 era_reward = -// EraReward::new(staker_reward_pool, era_info.total_staked_amount()); - -// // Switch to `Voting` period if conditions are met. -// if protocol_state.period_info.is_next_period(next_era) { -// // Store info about period end -// let bonus_reward_pool = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet -// PeriodEnd::::insert( -// &protocol_state.period_number(), -// PeriodEndInfo { -// bonus_reward_pool, -// total_vp_stake: era_info.staked_amount(PeriodType::Voting), -// final_era: current_era, -// }, -// ); - -// // For the sake of consistency we treat the whole `Voting` period as a single era. -// // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. -// let ending_era = next_era.saturating_add(1); -// let voting_period_length = Self::blocks_per_voting_period(); -// let next_era_start_block = now.saturating_add(voting_period_length); - -// protocol_state.next_period_type(ending_era, next_era_start_block); - -// era_info.migrate_to_next_era(Some(protocol_state.period_type())); - -// // TODO: trigger tier configuration calculation based on internal & external params. - -// ( -// Some(Event::::NewPeriod { -// period_type: protocol_state.period_type(), -// number: protocol_state.period_number(), -// }), -// era_reward, -// ) -// } else { -// let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); -// protocol_state.next_era_start = next_era_start_block; - -// era_info.migrate_to_next_era(None); - -// (None, era_reward) -// } -// } -// }; - -// // Update storage items - -// protocol_state.era = next_era; -// ActiveProtocolState::::put(protocol_state); - -// CurrentEraInfo::::put(era_info); - -// let era_span_index = Self::era_reward_span_index(current_era); -// let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); -// // TODO: error must not happen here. Log an error if it does. -// // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. -// // Will require a fix from the runtime team though. -// let _ = span.push(current_era, era_reward); -// EraRewards::::insert(&era_span_index, span); - -// Self::deposit_event(Event::::NewEra { era: next_era }); -// if let Some(period_event) = maybe_period_event { -// Self::deposit_event(period_event); -// } - -// // TODO: benchmark later -// T::DbWeight::get().reads_writes(3, 3) -// } -// } - -// #[pallet::call] -// impl Pallet { -// /// Used to enable or disable maintenance mode. -// /// Can only be called by manager origin. -// #[pallet::call_index(0)] -// #[pallet::weight(Weight::zero())] -// pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { -// T::ManagerOrigin::ensure_origin(origin)?; -// ActiveProtocolState::::mutate(|state| state.maintenance = enabled); -// Ok(()) -// } - -// /// Used to register a new contract for dApp staking. -// /// -// /// If successful, smart contract will be assigned a simple, unique numerical identifier. -// #[pallet::call_index(1)] -// #[pallet::weight(Weight::zero())] -// pub fn register( -// origin: OriginFor, -// owner: T::AccountId, -// smart_contract: T::SmartContract, -// ) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// T::ManagerOrigin::ensure_origin(origin)?; - -// ensure!( -// !IntegratedDApps::::contains_key(&smart_contract), -// Error::::ContractAlreadyExists, -// ); - -// ensure!( -// IntegratedDApps::::count() < T::MaxNumberOfContracts::get().into(), -// Error::::ExceededMaxNumberOfContracts -// ); - -// let dapp_id = NextDAppId::::get(); -// // MAX value must never be assigned as a dApp Id since it serves as a sentinel value. -// ensure!(dapp_id < DAppId::MAX, Error::::NewDAppIdUnavailable); - -// IntegratedDApps::::insert( -// &smart_contract, -// DAppInfo { -// owner: owner.clone(), -// id: dapp_id, -// state: DAppState::Registered, -// reward_destination: None, -// }, -// ); - -// NextDAppId::::put(dapp_id.saturating_add(1)); - -// Self::deposit_event(Event::::DAppRegistered { -// owner, -// smart_contract, -// dapp_id, -// }); - -// Ok(()) -// } - -// /// Used to modify the reward destination account for a dApp. -// /// -// /// Caller has to be dApp owner. -// /// If set to `None`, rewards will be deposited to the dApp owner. -// #[pallet::call_index(2)] -// #[pallet::weight(Weight::zero())] -// pub fn set_dapp_reward_destination( -// origin: OriginFor, -// smart_contract: T::SmartContract, -// beneficiary: Option, -// ) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let dev_account = ensure_signed(origin)?; - -// IntegratedDApps::::try_mutate( -// &smart_contract, -// |maybe_dapp_info| -> DispatchResult { -// let dapp_info = maybe_dapp_info -// .as_mut() -// .ok_or(Error::::ContractNotFound)?; - -// ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner); - -// dapp_info.reward_destination = beneficiary.clone(); - -// Ok(()) -// }, -// )?; - -// Self::deposit_event(Event::::DAppRewardDestinationUpdated { -// smart_contract, -// beneficiary, -// }); - -// Ok(()) -// } - -// /// Used to change dApp owner. -// /// -// /// Can be called by dApp owner or dApp staking manager origin. -// /// This is useful in two cases: -// /// 1. when the dApp owner account is compromised, manager can change the owner to a new account -// /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). -// #[pallet::call_index(3)] -// #[pallet::weight(Weight::zero())] -// pub fn set_dapp_owner( -// origin: OriginFor, -// smart_contract: T::SmartContract, -// new_owner: T::AccountId, -// ) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let origin = Self::ensure_signed_or_manager(origin)?; - -// IntegratedDApps::::try_mutate( -// &smart_contract, -// |maybe_dapp_info| -> DispatchResult { -// let dapp_info = maybe_dapp_info -// .as_mut() -// .ok_or(Error::::ContractNotFound)?; - -// // If manager origin, `None`, no need to check if caller is the owner. -// if let Some(caller) = origin { -// ensure!(dapp_info.owner == caller, Error::::OriginNotOwner); -// } - -// dapp_info.owner = new_owner.clone(); - -// Ok(()) -// }, -// )?; - -// Self::deposit_event(Event::::DAppOwnerChanged { -// smart_contract, -// new_owner, -// }); - -// Ok(()) -// } - -// /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. -// /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. -// /// -// /// Can be called by dApp owner or dApp staking manager origin. -// #[pallet::call_index(4)] -// #[pallet::weight(Weight::zero())] -// pub fn unregister( -// origin: OriginFor, -// smart_contract: T::SmartContract, -// ) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// T::ManagerOrigin::ensure_origin(origin)?; - -// let current_era = ActiveProtocolState::::get().era; - -// IntegratedDApps::::try_mutate( -// &smart_contract, -// |maybe_dapp_info| -> DispatchResult { -// let dapp_info = maybe_dapp_info -// .as_mut() -// .ok_or(Error::::ContractNotFound)?; - -// ensure!( -// dapp_info.state == DAppState::Registered, -// Error::::NotOperatedDApp -// ); - -// dapp_info.state = DAppState::Unregistered(current_era); - -// Ok(()) -// }, -// )?; - -// // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. - -// // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' - -// // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' - -// Self::deposit_event(Event::::DAppUnregistered { -// smart_contract, -// era: current_era, -// }); - -// Ok(()) -// } - -// /// Locks additional funds into dApp staking. -// /// -// /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. -// /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. -// /// -// /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, -// /// caller should claim pending rewards, before retrying to lock additional funds. -// #[pallet::call_index(5)] -// #[pallet::weight(Weight::zero())] -// pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// let mut ledger = Ledger::::get(&account); - -// // Calculate & check amount available for locking -// let available_balance = -// T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); -// let amount_to_lock = available_balance.min(amount); -// ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); - -// ledger.add_lock_amount(amount_to_lock); - -// ensure!( -// ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), -// Error::::LockedAmountBelowThreshold -// ); - -// Self::update_ledger(&account, ledger); -// CurrentEraInfo::::mutate(|era_info| { -// era_info.add_locked(amount_to_lock); -// }); - -// Self::deposit_event(Event::::Locked { -// account, -// amount: amount_to_lock, -// }); - -// Ok(()) -// } - -// /// Attempts to start the unlocking process for the specified amount. -// /// -// /// Only the amount that isn't actively used for staking can be unlocked. -// /// If the amount is greater than the available amount for unlocking, everything is unlocked. -// /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. -// #[pallet::call_index(6)] -// #[pallet::weight(Weight::zero())] -// pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// let state = ActiveProtocolState::::get(); -// let mut ledger = Ledger::::get(&account); - -// let available_for_unlocking = ledger.unlockable_amount(state.period_info.number); -// let amount_to_unlock = available_for_unlocking.min(amount); - -// // Ensure we unlock everything if remaining amount is below threshold. -// let remaining_amount = ledger -// .active_locked_amount() -// .saturating_sub(amount_to_unlock); -// let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { -// ensure!( -// ledger.active_stake(state.period_info.number).is_zero(), -// Error::::RemainingStakePreventsFullUnlock -// ); -// ledger.active_locked_amount() -// } else { -// amount_to_unlock -// }; - -// // Sanity check -// ensure!(!amount_to_unlock.is_zero(), Error::::ZeroAmount); - -// // Update ledger with new lock and unlocking amounts -// ledger.subtract_lock_amount(amount_to_unlock); - -// let current_block = frame_system::Pallet::::block_number(); -// let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); -// ledger -// .add_unlocking_chunk(amount_to_unlock, unlock_block) -// .map_err(|_| Error::::TooManyUnlockingChunks)?; - -// // Update storage -// Self::update_ledger(&account, ledger); -// CurrentEraInfo::::mutate(|era_info| { -// era_info.unlocking_started(amount_to_unlock); -// }); - -// Self::deposit_event(Event::::Unlocking { -// account, -// amount: amount_to_unlock, -// }); - -// Ok(()) -// } - -// /// Claims all of fully unlocked chunks, removing the lock from them. -// #[pallet::call_index(7)] -// #[pallet::weight(Weight::zero())] -// pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// let mut ledger = Ledger::::get(&account); - -// let current_block = frame_system::Pallet::::block_number(); -// let amount = ledger.claim_unlocked(current_block); -// ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); - -// Self::update_ledger(&account, ledger); -// CurrentEraInfo::::mutate(|era_info| { -// era_info.unlocking_removed(amount); -// }); - -// // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) - -// // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have - -// Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); - -// Ok(()) -// } - -// #[pallet::call_index(8)] -// #[pallet::weight(Weight::zero())] -// pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// let mut ledger = Ledger::::get(&account); - -// ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); - -// let amount = ledger.consume_unlocking_chunks(); - -// ledger.add_lock_amount(amount); -// ensure!( -// ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), -// Error::::LockedAmountBelowThreshold -// ); - -// Self::update_ledger(&account, ledger); -// CurrentEraInfo::::mutate(|era_info| { -// era_info.add_locked(amount); -// era_info.unlocking_removed(amount); -// }); - -// Self::deposit_event(Event::::Relock { account, amount }); - -// Ok(()) -// } - -// /// Stake the specified amount on a smart contract. -// /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. -// /// -// /// Depending on the period type, appropriate stake amount will be updated. -// #[pallet::call_index(9)] -// #[pallet::weight(Weight::zero())] -// pub fn stake( -// origin: OriginFor, -// smart_contract: T::SmartContract, -// #[pallet::compact] amount: Balance, -// ) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// ensure!(amount > 0, Error::::ZeroAmount); - -// ensure!( -// Self::is_active(&smart_contract), -// Error::::NotOperatedDApp -// ); - -// let protocol_state = ActiveProtocolState::::get(); -// // Staker always stakes from the NEXT era -// let stake_era = protocol_state.era.saturating_add(1); -// ensure!( -// !protocol_state.period_info.is_next_period(stake_era), -// Error::::PeriodEndsInNextEra -// ); - -// let mut ledger = Ledger::::get(&account); - -// // 1. -// // Increase stake amount for the next era & current period in staker's ledger -// ledger -// .add_stake_amount(amount, stake_era, protocol_state.period_number()) -// .map_err(|err| match err { -// AccountLedgerError::InvalidPeriod => { -// Error::::UnclaimedRewardsFromPastPeriods -// } -// AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, -// AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, -// // Defensive check, should never happen -// _ => Error::::InternalStakeError, -// })?; - -// // 2. -// // Update `StakerInfo` storage with the new stake amount on the specified contract. -// // -// // There are two distinct scenarios: -// // 1. Existing entry matches the current period number - just update it. -// // 2. Entry doesn't exist or it's for an older period - create a new one. -// // -// // This is ok since we only use this storage entry to keep track of how much each staker -// // has staked on each contract in the current period. We only ever need the latest information. -// // This is because `AccountLedger` is the one keeping information about how much was staked when. -// let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { -// Some(mut staking_info) -// if staking_info.period_number() == protocol_state.period_number() => -// { -// staking_info.stake(amount, protocol_state.period_info.period_type); -// staking_info -// } -// _ => { -// ensure!( -// amount >= T::MinimumStakeAmount::get(), -// Error::::InsufficientStakeAmount -// ); -// let mut staking_info = SingularStakingInfo::new( -// protocol_state.period_info.number, -// protocol_state.period_info.period_type, -// ); -// staking_info.stake(amount, protocol_state.period_info.period_type); -// staking_info -// } -// }; - -// // 3. -// // Update `ContractStake` storage with the new stake amount on the specified contract. -// let mut contract_stake_info = ContractStake::::get(&smart_contract); -// ensure!( -// contract_stake_info -// .stake(amount, protocol_state.period_info, stake_era) -// .is_ok(), -// Error::::InternalStakeError -// ); - -// // 4. -// // Update total staked amount for the next era. -// CurrentEraInfo::::mutate(|era_info| { -// era_info.add_stake_amount(amount, protocol_state.period_type()); -// }); - -// // 5. -// // Update remaining storage entries -// Self::update_ledger(&account, ledger); -// StakerInfo::::insert(&account, &smart_contract, new_staking_info); -// ContractStake::::insert(&smart_contract, contract_stake_info); - -// Self::deposit_event(Event::::Stake { -// account, -// smart_contract, -// amount, -// }); - -// Ok(()) -// } - -// /// Unstake the specified amount from a smart contract. -// /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. -// /// -// /// Depending on the period type, appropriate stake amount will be updated. -// #[pallet::call_index(10)] -// #[pallet::weight(Weight::zero())] -// pub fn unstake( -// origin: OriginFor, -// smart_contract: T::SmartContract, -// #[pallet::compact] amount: Balance, -// ) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// ensure!(amount > 0, Error::::ZeroAmount); - -// ensure!( -// Self::is_active(&smart_contract), -// Error::::NotOperatedDApp -// ); - -// let protocol_state = ActiveProtocolState::::get(); -// let unstake_era = protocol_state.era; - -// let mut ledger = Ledger::::get(&account); - -// // 1. -// // Update `StakerInfo` storage with the reduced stake amount on the specified contract. -// let (new_staking_info, amount) = match StakerInfo::::get(&account, &smart_contract) { -// Some(mut staking_info) => { -// ensure!( -// staking_info.period_number() == protocol_state.period_number(), -// Error::::UnstakeFromPastPeriod -// ); -// ensure!( -// staking_info.total_staked_amount() >= amount, -// Error::::UnstakeAmountTooLarge -// ); - -// // If unstaking would take the total staked amount below the minimum required value, -// // unstake everything. -// let amount = if staking_info.total_staked_amount().saturating_sub(amount) -// < T::MinimumStakeAmount::get() -// { -// staking_info.total_staked_amount() -// } else { -// amount -// }; - -// staking_info.unstake(amount, protocol_state.period_type()); -// (staking_info, amount) -// } -// None => { -// return Err(Error::::NoStakingInfo.into()); -// } -// }; - -// // 2. -// // Reduce stake amount -// ledger -// .unstake_amount(amount, unstake_era, protocol_state.period_number()) -// .map_err(|err| match err { -// AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, -// AccountLedgerError::UnstakeAmountLargerThanStake => { -// Error::::UnstakeAmountTooLarge -// } -// AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, -// _ => Error::::InternalUnstakeError, -// })?; - -// // 3. -// // Update `ContractStake` storage with the reduced stake amount on the specified contract. -// let mut contract_stake_info = ContractStake::::get(&smart_contract); -// ensure!( -// contract_stake_info -// .unstake(amount, protocol_state.period_info, unstake_era) -// .is_ok(), -// Error::::InternalUnstakeError -// ); - -// // 4. -// // Update total staked amount for the next era. -// CurrentEraInfo::::mutate(|era_info| { -// era_info.unstake_amount(amount, protocol_state.period_type()); -// }); - -// // 5. -// // Update remaining storage entries -// Self::update_ledger(&account, ledger); -// ContractStake::::insert(&smart_contract, contract_stake_info); - -// if new_staking_info.is_empty() { -// StakerInfo::::remove(&account, &smart_contract); -// } else { -// StakerInfo::::insert(&account, &smart_contract, new_staking_info); -// } - -// Self::deposit_event(Event::::Unstake { -// account, -// smart_contract, -// amount, -// }); - -// Ok(()) -// } - -// /// TODO -// #[pallet::call_index(11)] -// #[pallet::weight(Weight::zero())] -// pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { -// Self::ensure_pallet_enabled()?; -// let account = ensure_signed(origin)?; - -// let protocol_state = ActiveProtocolState::::get(); -// let mut ledger = Ledger::::get(&account); - -// // TODO: how do we handle expired rewards? Add an additional call to clean them up? -// // Putting this logic inside existing calls will add even more complexity. - -// // Check if the rewards have expired -// let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; -// ensure!( -// staked_period -// >= protocol_state -// .period_number() -// .saturating_sub(T::RewardRetentionInPeriods::get()), -// Error::::StakerRewardsExpired -// ); - -// // Calculate the reward claim span -// let (first_chunk, last_chunk) = ledger -// .first_and_last_stake_chunks() -// .ok_or(Error::::InternalClaimStakerError)?; -// let era_rewards = -// EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) -// .ok_or(Error::::NoClaimableRewards)?; - -// // The last era for which we can theoretically claim rewards. -// // And indicator if we know the period's ending era. -// let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { -// (protocol_state.era.saturating_sub(1), None) -// } else { -// PeriodEnd::::get(&staked_period) -// .map(|info| (info.final_era, Some(info.final_era))) -// .ok_or(Error::::InternalClaimStakerError)? -// }; - -// // The last era for which we can claim rewards for this account. -// // Limiting factors are: -// // 1. era reward span entries -// // 2. last era in period limit -// // 3. last era in which staker had non-zero stake -// let last_claim_era = if last_chunk.get_amount().is_zero() { -// era_rewards -// .last_era() -// .min(last_period_era) -// .min(last_chunk.get_era()) -// } else { -// era_rewards.last_era().min(last_period_era) -// }; - -// // Get chunks for reward claiming -// let chunks_for_claim = -// ledger -// .claim_up_to_era(last_claim_era, period_end) -// .map_err(|err| match err { -// AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, -// _ => Error::::InternalClaimStakerError, -// })?; - -// // Calculate rewards -// let mut rewards: Vec<_> = Vec::new(); -// let mut reward_sum = Balance::zero(); -// for era in first_chunk.get_era()..=last_claim_era { -// // TODO: this should be zipped, and values should be fetched only once -// let era_reward = era_rewards -// .get(era) -// .ok_or(Error::::InternalClaimStakerError)?; - -// let chunk = chunks_for_claim -// .get(era) -// .ok_or(Error::::InternalClaimStakerError)?; - -// // Optimization, and zero-division protection -// if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { -// continue; -// } -// let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) -// * era_reward.staker_reward_pool(); - -// rewards.push((era, staker_reward)); -// reward_sum.saturating_accrue(staker_reward); -// } - -// T::Currency::deposit_into_existing(&account, reward_sum) -// .map_err(|_| Error::::InternalClaimStakerError)?; - -// Self::update_ledger(&account, ledger); - -// rewards.into_iter().for_each(|(era, reward)| { -// Self::deposit_event(Event::::Reward { -// account: account.clone(), -// era, -// amount: reward, -// }); -// }); - -// Ok(()) -// } -// } - -// impl Pallet { -// /// `Err` if pallet disabled for maintenance, `Ok` otherwise. -// pub(crate) fn ensure_pallet_enabled() -> Result<(), Error> { -// if ActiveProtocolState::::get().maintenance { -// Err(Error::::Disabled) -// } else { -// Ok(()) -// } -// } - -// /// Ensure that the origin is either the `ManagerOrigin` or a signed origin. -// /// -// /// In case of manager, `Ok(None)` is returned, and if signed origin `Ok(Some(AccountId))` is returned. -// pub(crate) fn ensure_signed_or_manager( -// origin: T::RuntimeOrigin, -// ) -> Result, BadOrigin> { -// if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() { -// return Ok(None); -// } -// let who = ensure_signed(origin)?; -// Ok(Some(who)) -// } - -// /// Update the account ledger, and dApp staking balance lock. -// /// -// /// In case account ledger is empty, entries from the DB are removed and lock is released. -// pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { -// if ledger.is_empty() { -// Ledger::::remove(&account); -// T::Currency::remove_lock(STAKING_ID, account); -// } else { -// T::Currency::set_lock( -// STAKING_ID, -// account, -// ledger.active_locked_amount(), -// WithdrawReasons::all(), -// ); -// Ledger::::insert(account, ledger); -// } -// } - -// /// Returns the number of blocks per voting period. -// pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { -// T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) -// } - -// /// `true` if smart contract is active, `false` if it has been unregistered. -// fn is_active(smart_contract: &T::SmartContract) -> bool { -// IntegratedDApps::::get(smart_contract) -// .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) -// } - -// /// Calculates the `EraRewardSpan` index for the specified era. -// pub fn era_reward_span_index(era: EraNumber) -> EraNumber { -// era.saturating_sub(era % T::EraRewardSpanLength::get()) -// } -// } -// } +#[frame_support::pallet] +pub mod pallet { + use super::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Currency used for staking. + /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) + type Currency: LockableCurrency< + Self::AccountId, + Moment = Self::BlockNumber, + Balance = Balance, + >; + + /// Describes smart contract in the context required by dApp staking. + type SmartContract: Parameter + Member + MaxEncodedLen; + + /// Privileged origin for managing dApp staking pallet. + type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; + + /// Length of a standard era in block numbers. + #[pallet::constant] + type StandardEraLength: Get; + + /// Length of the `Voting` period in standard eras. + /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras + /// for the sake of simplicity & consistency. + #[pallet::constant] + type StandardErasPerVotingPeriod: Get; + + /// Length of the `Build&Earn` period in standard eras. + /// Each `Build&Earn` period consists of one or more distinct standard eras. + #[pallet::constant] + type StandardErasPerBuildAndEarnPeriod: Get; + + /// Maximum length of a single era reward span length entry. + #[pallet::constant] + type EraRewardSpanLength: Get; + + /// Number of periods for which we keep rewards available for claiming. + /// After that period, they are no longer claimable. + #[pallet::constant] + type RewardRetentionInPeriods: Get; + + /// Maximum number of contracts that can be integrated into dApp staking at once. + #[pallet::constant] + type MaxNumberOfContracts: Get; + + /// Maximum number of unlocking chunks that can exist per account at a time. + #[pallet::constant] + type MaxUnlockingChunks: Get; + + /// Minimum amount an account has to lock in dApp staking in order to participate. + #[pallet::constant] + type MinimumLockedAmount: Get; + + /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. + #[pallet::constant] + type UnlockingPeriod: Get>; + + /// Minimum amount staker can stake on a contract. + #[pallet::constant] + type MinimumStakeAmount: Get; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// New era has started. + NewEra { era: EraNumber }, + /// New period has started. + NewPeriod { + period_type: PeriodType, + number: PeriodNumber, + }, + /// A smart contract has been registered for dApp staking + DAppRegistered { + owner: T::AccountId, + smart_contract: T::SmartContract, + dapp_id: DAppId, + }, + /// dApp reward destination has been updated. + DAppRewardDestinationUpdated { + smart_contract: T::SmartContract, + beneficiary: Option, + }, + /// dApp owner has been changed. + DAppOwnerChanged { + smart_contract: T::SmartContract, + new_owner: T::AccountId, + }, + /// dApp has been unregistered + DAppUnregistered { + smart_contract: T::SmartContract, + era: EraNumber, + }, + /// Account has locked some amount into dApp staking. + Locked { + account: T::AccountId, + amount: Balance, + }, + /// Account has started the unlocking process for some amount. + Unlocking { + account: T::AccountId, + amount: Balance, + }, + /// Account has claimed unlocked amount, removing the lock from it. + ClaimedUnlocked { + account: T::AccountId, + amount: Balance, + }, + /// Account has relocked all of the unlocking chunks. + Relock { + account: T::AccountId, + amount: Balance, + }, + /// Account has staked some amount on a smart contract. + Stake { + account: T::AccountId, + smart_contract: T::SmartContract, + amount: Balance, + }, + /// Account has unstaked some amount from a smart contract. + Unstake { + account: T::AccountId, + smart_contract: T::SmartContract, + amount: Balance, + }, + /// Account has claimed some stake rewards. + Reward { + account: T::AccountId, + era: EraNumber, + amount: Balance, + }, + } + + #[pallet::error] + pub enum Error { + /// Pallet is disabled/in maintenance mode. + Disabled, + /// Smart contract already exists within dApp staking protocol. + ContractAlreadyExists, + /// Maximum number of smart contracts has been reached. + ExceededMaxNumberOfContracts, + /// Not possible to assign a new dApp Id. + /// This should never happen since current type can support up to 65536 - 1 unique dApps. + NewDAppIdUnavailable, + /// Specified smart contract does not exist in dApp staking. + ContractNotFound, + /// Call origin is not dApp owner. + OriginNotOwner, + /// dApp is part of dApp staking but isn't active anymore. + NotOperatedDApp, + /// Performing locking or staking with 0 amount. + ZeroAmount, + /// Total locked amount for staker is below minimum threshold. + LockedAmountBelowThreshold, + /// Cannot add additional unlocking chunks due to capacity limit. + TooManyUnlockingChunks, + /// Remaining stake prevents entire balance of starting the unlocking process. + RemainingStakePreventsFullUnlock, + /// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all. + NoUnlockedChunksToClaim, + /// There are no unlocking chunks available to relock. + NoUnlockingChunks, + /// The amount being staked is too large compared to what's available for staking. + UnavailableStakeFunds, + /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. + UnclaimedRewardsFromPastPeriods, + /// An unexpected error occured while trying to stake. + InternalStakeError, + /// Total staked amount on contract is below the minimum required value. + InsufficientStakeAmount, + /// Stake operation is rejected since period ends in the next era. + PeriodEndsInNextEra, + /// Unstaking is rejected since the period in which past stake was active has passed. + UnstakeFromPastPeriod, + /// Unstake amount is greater than the staked amount. + UnstakeAmountTooLarge, + /// Account has no staking information for the contract. + NoStakingInfo, + /// An unexpected error occured while trying to unstake. + InternalUnstakeError, + /// Rewards are no longer claimable since they are too old. + StakerRewardsExpired, + /// There are no claimable rewards for the account. + NoClaimableRewards, + /// An unexpected error occured while trying to claim rewards. + InternalClaimStakerError, + } + + /// General information about dApp staking protocol state. + #[pallet::storage] + pub type ActiveProtocolState = + StorageValue<_, ProtocolState>, ValueQuery>; + + /// Counter for unique dApp identifiers. + #[pallet::storage] + pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; + + /// Map of all dApps integrated into dApp staking protocol. + #[pallet::storage] + pub type IntegratedDApps = CountedStorageMap< + _, + Blake2_128Concat, + T::SmartContract, + DAppInfo, + OptionQuery, + >; + + /// General locked/staked information for each account. + #[pallet::storage] + pub type Ledger = + StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; + + /// Information about how much each staker has staked for each smart contract in some period. + #[pallet::storage] + pub type StakerInfo = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + T::SmartContract, + SingularStakingInfo, + OptionQuery, + >; + + /// Information about how much has been staked on a smart contract in some era or period. + #[pallet::storage] + pub type ContractStake = + StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakeAmountSeries, ValueQuery>; + + /// General information about the current era. + #[pallet::storage] + pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; + + /// Information about rewards for each era. + /// + /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. + /// For the sake of simplicity, valid `era` keys are calculated as: + /// + /// era_key = era - (era % T::EraRewardSpanLength) + /// + /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. + /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. + #[pallet::storage] + pub type EraRewards = + StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan, OptionQuery>; + + /// Information about period's end. + #[pallet::storage] + pub type PeriodEnd = + StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let mut protocol_state = ActiveProtocolState::::get(); + + // We should not modify pallet storage while in maintenance mode. + // This is a safety measure, since maintenance mode is expected to be + // enabled in case some misbehavior or corrupted storage is detected. + if protocol_state.maintenance { + return T::DbWeight::get().reads(1); + } + + // Nothing to do if it's not new era + if !protocol_state.is_new_era(now) { + return T::DbWeight::get().reads(1); + } + + let mut era_info = CurrentEraInfo::::get(); + + let current_era = protocol_state.era; + let next_era = current_era.saturating_add(1); + let (maybe_period_event, era_reward) = match protocol_state.period_type() { + PeriodType::Voting => { + // For the sake of consistency, we put zero reward into storage + let era_reward = + EraReward::new(Balance::zero(), era_info.total_staked_amount()); + + let ending_era = + next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); + let build_and_earn_start_block = + now.saturating_add(T::StandardEraLength::get()); + protocol_state.next_period_type(ending_era, build_and_earn_start_block); + + era_info.migrate_to_next_era(Some(protocol_state.period_type())); + + ( + Some(Event::::NewPeriod { + period_type: protocol_state.period_type(), + number: protocol_state.period_number(), + }), + era_reward, + ) + } + 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 era_reward = + EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + + // Switch to `Voting` period if conditions are met. + if protocol_state.period_info.is_next_period(next_era) { + // Store info about period end + let bonus_reward_pool = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet + PeriodEnd::::insert( + &protocol_state.period_number(), + PeriodEndInfo { + bonus_reward_pool, + total_vp_stake: era_info.staked_amount(PeriodType::Voting), + final_era: current_era, + }, + ); + + // For the sake of consistency we treat the whole `Voting` period as a single era. + // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. + let ending_era = next_era.saturating_add(1); + let voting_period_length = Self::blocks_per_voting_period(); + let next_era_start_block = now.saturating_add(voting_period_length); + + protocol_state.next_period_type(ending_era, next_era_start_block); + + era_info.migrate_to_next_era(Some(protocol_state.period_type())); + + // TODO: trigger tier configuration calculation based on internal & external params. + + ( + Some(Event::::NewPeriod { + period_type: protocol_state.period_type(), + number: protocol_state.period_number(), + }), + era_reward, + ) + } else { + let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); + protocol_state.next_era_start = next_era_start_block; + + era_info.migrate_to_next_era(None); + + (None, era_reward) + } + } + }; + + // Update storage items + + protocol_state.era = next_era; + ActiveProtocolState::::put(protocol_state); + + CurrentEraInfo::::put(era_info); + + let era_span_index = Self::era_reward_span_index(current_era); + let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); + // TODO: error must not happen here. Log an error if it does. + // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. + // Will require a fix from the runtime team though. + let _ = span.push(current_era, era_reward); + EraRewards::::insert(&era_span_index, span); + + Self::deposit_event(Event::::NewEra { era: next_era }); + if let Some(period_event) = maybe_period_event { + Self::deposit_event(period_event); + } + + // TODO: benchmark later + T::DbWeight::get().reads_writes(3, 3) + } + } + + #[pallet::call] + impl Pallet { + /// Used to enable or disable maintenance mode. + /// Can only be called by manager origin. + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] + pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { + T::ManagerOrigin::ensure_origin(origin)?; + ActiveProtocolState::::mutate(|state| state.maintenance = enabled); + Ok(()) + } + + /// Used to register a new contract for dApp staking. + /// + /// If successful, smart contract will be assigned a simple, unique numerical identifier. + #[pallet::call_index(1)] + #[pallet::weight(Weight::zero())] + pub fn register( + origin: OriginFor, + owner: T::AccountId, + smart_contract: T::SmartContract, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + T::ManagerOrigin::ensure_origin(origin)?; + + ensure!( + !IntegratedDApps::::contains_key(&smart_contract), + Error::::ContractAlreadyExists, + ); + + ensure!( + IntegratedDApps::::count() < T::MaxNumberOfContracts::get().into(), + Error::::ExceededMaxNumberOfContracts + ); + + let dapp_id = NextDAppId::::get(); + // MAX value must never be assigned as a dApp Id since it serves as a sentinel value. + ensure!(dapp_id < DAppId::MAX, Error::::NewDAppIdUnavailable); + + IntegratedDApps::::insert( + &smart_contract, + DAppInfo { + owner: owner.clone(), + id: dapp_id, + state: DAppState::Registered, + reward_destination: None, + }, + ); + + NextDAppId::::put(dapp_id.saturating_add(1)); + + Self::deposit_event(Event::::DAppRegistered { + owner, + smart_contract, + dapp_id, + }); + + Ok(()) + } + + /// Used to modify the reward destination account for a dApp. + /// + /// Caller has to be dApp owner. + /// If set to `None`, rewards will be deposited to the dApp owner. + #[pallet::call_index(2)] + #[pallet::weight(Weight::zero())] + pub fn set_dapp_reward_destination( + origin: OriginFor, + smart_contract: T::SmartContract, + beneficiary: Option, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let dev_account = ensure_signed(origin)?; + + IntegratedDApps::::try_mutate( + &smart_contract, + |maybe_dapp_info| -> DispatchResult { + let dapp_info = maybe_dapp_info + .as_mut() + .ok_or(Error::::ContractNotFound)?; + + ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner); + + dapp_info.reward_destination = beneficiary.clone(); + + Ok(()) + }, + )?; + + Self::deposit_event(Event::::DAppRewardDestinationUpdated { + smart_contract, + beneficiary, + }); + + Ok(()) + } + + /// Used to change dApp owner. + /// + /// Can be called by dApp owner or dApp staking manager origin. + /// This is useful in two cases: + /// 1. when the dApp owner account is compromised, manager can change the owner to a new account + /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). + #[pallet::call_index(3)] + #[pallet::weight(Weight::zero())] + pub fn set_dapp_owner( + origin: OriginFor, + smart_contract: T::SmartContract, + new_owner: T::AccountId, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let origin = Self::ensure_signed_or_manager(origin)?; + + IntegratedDApps::::try_mutate( + &smart_contract, + |maybe_dapp_info| -> DispatchResult { + let dapp_info = maybe_dapp_info + .as_mut() + .ok_or(Error::::ContractNotFound)?; + + // If manager origin, `None`, no need to check if caller is the owner. + if let Some(caller) = origin { + ensure!(dapp_info.owner == caller, Error::::OriginNotOwner); + } + + dapp_info.owner = new_owner.clone(); + + Ok(()) + }, + )?; + + Self::deposit_event(Event::::DAppOwnerChanged { + smart_contract, + new_owner, + }); + + Ok(()) + } + + /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. + /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. + /// + /// Can be called by dApp owner or dApp staking manager origin. + #[pallet::call_index(4)] + #[pallet::weight(Weight::zero())] + pub fn unregister( + origin: OriginFor, + smart_contract: T::SmartContract, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + T::ManagerOrigin::ensure_origin(origin)?; + + let current_era = ActiveProtocolState::::get().era; + + IntegratedDApps::::try_mutate( + &smart_contract, + |maybe_dapp_info| -> DispatchResult { + let dapp_info = maybe_dapp_info + .as_mut() + .ok_or(Error::::ContractNotFound)?; + + ensure!( + dapp_info.state == DAppState::Registered, + Error::::NotOperatedDApp + ); + + dapp_info.state = DAppState::Unregistered(current_era); + + Ok(()) + }, + )?; + + // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. + + // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' + + // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' + + Self::deposit_event(Event::::DAppUnregistered { + smart_contract, + era: current_era, + }); + + Ok(()) + } + + /// Locks additional funds into dApp staking. + /// + /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. + /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. + /// + /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, + /// caller should claim pending rewards, before retrying to lock additional funds. + #[pallet::call_index(5)] + #[pallet::weight(Weight::zero())] + pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let mut ledger = Ledger::::get(&account); + + // Calculate & check amount available for locking + let available_balance = + T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); + let amount_to_lock = available_balance.min(amount); + ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); + + ledger.add_lock_amount(amount_to_lock); + + ensure!( + ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), + Error::::LockedAmountBelowThreshold + ); + + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.add_locked(amount_to_lock); + }); + + Self::deposit_event(Event::::Locked { + account, + amount: amount_to_lock, + }); + + Ok(()) + } + + /// Attempts to start the unlocking process for the specified amount. + /// + /// Only the amount that isn't actively used for staking can be unlocked. + /// If the amount is greater than the available amount for unlocking, everything is unlocked. + /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. + #[pallet::call_index(6)] + #[pallet::weight(Weight::zero())] + pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + let available_for_unlocking = ledger.unlockable_amount(state.period_info.number); + let amount_to_unlock = available_for_unlocking.min(amount); + + // Ensure we unlock everything if remaining amount is below threshold. + let remaining_amount = ledger + .active_locked_amount() + .saturating_sub(amount_to_unlock); + let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { + ensure!( + ledger.staked_amount(state.period_info.number).is_zero(), + Error::::RemainingStakePreventsFullUnlock + ); + ledger.active_locked_amount() + } else { + amount_to_unlock + }; + + // Sanity check + ensure!(!amount_to_unlock.is_zero(), Error::::ZeroAmount); + + // Update ledger with new lock and unlocking amounts + ledger.subtract_lock_amount(amount_to_unlock); + + let current_block = frame_system::Pallet::::block_number(); + let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); + ledger + .add_unlocking_chunk(amount_to_unlock, unlock_block) + .map_err(|_| Error::::TooManyUnlockingChunks)?; + + // Update storage + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.unlocking_started(amount_to_unlock); + }); + + Self::deposit_event(Event::::Unlocking { + account, + amount: amount_to_unlock, + }); + + Ok(()) + } + + /// Claims all of fully unlocked chunks, removing the lock from them. + #[pallet::call_index(7)] + #[pallet::weight(Weight::zero())] + pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let mut ledger = Ledger::::get(&account); + + let current_block = frame_system::Pallet::::block_number(); + let amount = ledger.claim_unlocked(current_block); + ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); + + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.unlocking_removed(amount); + }); + + // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) + + // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have + + Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); + + Ok(()) + } + + #[pallet::call_index(8)] + #[pallet::weight(Weight::zero())] + pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let mut ledger = Ledger::::get(&account); + + ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); + + let amount = ledger.consume_unlocking_chunks(); + + ledger.add_lock_amount(amount); + ensure!( + ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), + Error::::LockedAmountBelowThreshold + ); + + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.add_locked(amount); + era_info.unlocking_removed(amount); + }); + + Self::deposit_event(Event::::Relock { account, amount }); + + Ok(()) + } + + /// Stake the specified amount on a smart contract. + /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. + /// + /// Depending on the period type, appropriate stake amount will be updated. + #[pallet::call_index(9)] + #[pallet::weight(Weight::zero())] + pub fn stake( + origin: OriginFor, + smart_contract: T::SmartContract, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + ensure!(amount > 0, Error::::ZeroAmount); + + ensure!( + Self::is_active(&smart_contract), + Error::::NotOperatedDApp + ); + + let protocol_state = ActiveProtocolState::::get(); + // Staker always stakes from the NEXT era + let stake_era = protocol_state.era.saturating_add(1); + ensure!( + !protocol_state.period_info.is_next_period(stake_era), + Error::::PeriodEndsInNextEra + ); + + let mut ledger = Ledger::::get(&account); + + // 1. + // Increase stake amount for the next era & current period in staker's ledger + ledger + .add_stake_amount(amount, stake_era, protocol_state.period_info) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => { + Error::::UnclaimedRewardsFromPastPeriods + } + AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, + // Defensive check, should never happen + _ => Error::::InternalStakeError, + })?; + + // 2. + // Update `StakerInfo` storage with the new stake amount on the specified contract. + // + // There are two distinct scenarios: + // 1. Existing entry matches the current period number - just update it. + // 2. Entry doesn't exist or it's for an older period - create a new one. + // + // This is ok since we only use this storage entry to keep track of how much each staker + // has staked on each contract in the current period. We only ever need the latest information. + // This is because `AccountLedger` is the one keeping information about how much was staked when. + let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { + Some(mut staking_info) + if staking_info.period_number() == protocol_state.period_number() => + { + staking_info.stake(amount, protocol_state.period_info.period_type); + staking_info + } + _ => { + ensure!( + amount >= T::MinimumStakeAmount::get(), + Error::::InsufficientStakeAmount + ); + let mut staking_info = SingularStakingInfo::new( + protocol_state.period_info.number, + protocol_state.period_info.period_type, + ); + staking_info.stake(amount, protocol_state.period_info.period_type); + staking_info + } + }; + + // 3. + // Update `ContractStake` storage with the new stake amount on the specified contract. + let mut contract_stake_info = ContractStake::::get(&smart_contract); + ensure!( + contract_stake_info + .stake(amount, protocol_state.period_info, stake_era) + .is_ok(), + Error::::InternalStakeError + ); + + // 4. + // Update total staked amount for the next era. + CurrentEraInfo::::mutate(|era_info| { + era_info.add_stake_amount(amount, protocol_state.period_type()); + }); + + // 5. + // Update remaining storage entries + Self::update_ledger(&account, ledger); + StakerInfo::::insert(&account, &smart_contract, new_staking_info); + ContractStake::::insert(&smart_contract, contract_stake_info); + + Self::deposit_event(Event::::Stake { + account, + smart_contract, + amount, + }); + + Ok(()) + } + + /// Unstake the specified amount from a smart contract. + /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. + /// + /// Depending on the period type, appropriate stake amount will be updated. + #[pallet::call_index(10)] + #[pallet::weight(Weight::zero())] + pub fn unstake( + origin: OriginFor, + smart_contract: T::SmartContract, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + ensure!(amount > 0, Error::::ZeroAmount); + + ensure!( + Self::is_active(&smart_contract), + Error::::NotOperatedDApp + ); + + let protocol_state = ActiveProtocolState::::get(); + let unstake_era = protocol_state.era; + + let mut ledger = Ledger::::get(&account); + + // 1. + // Update `StakerInfo` storage with the reduced stake amount on the specified contract. + let (new_staking_info, amount) = match StakerInfo::::get(&account, &smart_contract) { + Some(mut staking_info) => { + ensure!( + staking_info.period_number() == protocol_state.period_number(), + Error::::UnstakeFromPastPeriod + ); + ensure!( + staking_info.total_staked_amount() >= amount, + Error::::UnstakeAmountTooLarge + ); + + // If unstaking would take the total staked amount below the minimum required value, + // unstake everything. + let amount = if staking_info.total_staked_amount().saturating_sub(amount) + < T::MinimumStakeAmount::get() + { + staking_info.total_staked_amount() + } else { + amount + }; + + staking_info.unstake(amount, protocol_state.period_type()); + (staking_info, amount) + } + None => { + return Err(Error::::NoStakingInfo.into()); + } + }; + + // 2. + // Reduce stake amount + ledger + .unstake_amount(amount, unstake_era, protocol_state.period_info) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, + AccountLedgerError::UnstakeAmountLargerThanStake => { + Error::::UnstakeAmountTooLarge + } + _ => Error::::InternalUnstakeError, + })?; + + // 3. + // Update `ContractStake` storage with the reduced stake amount on the specified contract. + let mut contract_stake_info = ContractStake::::get(&smart_contract); + ensure!( + contract_stake_info + .unstake(amount, protocol_state.period_info, unstake_era) + .is_ok(), + Error::::InternalUnstakeError + ); + + // 4. + // Update total staked amount for the next era. + CurrentEraInfo::::mutate(|era_info| { + era_info.unstake_amount(amount, protocol_state.period_type()); + }); + + // 5. + // Update remaining storage entries + Self::update_ledger(&account, ledger); + ContractStake::::insert(&smart_contract, contract_stake_info); + + if new_staking_info.is_empty() { + StakerInfo::::remove(&account, &smart_contract); + } else { + StakerInfo::::insert(&account, &smart_contract, new_staking_info); + } + + Self::deposit_event(Event::::Unstake { + account, + smart_contract, + amount, + }); + + Ok(()) + } + + // /// TODO + // #[pallet::call_index(11)] + // #[pallet::weight(Weight::zero())] + // pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { + // Self::ensure_pallet_enabled()?; + // let account = ensure_signed(origin)?; + + // let protocol_state = ActiveProtocolState::::get(); + // let mut ledger = Ledger::::get(&account); + + // // TODO: how do we handle expired rewards? Add an additional call to clean them up? + // // Putting this logic inside existing calls will add even more complexity. + + // // Check if the rewards have expired + // let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; + // ensure!( + // staked_period + // >= protocol_state + // .period_number() + // .saturating_sub(T::RewardRetentionInPeriods::get()), + // Error::::StakerRewardsExpired + // ); + + // // Calculate the reward claim span + // let (first_chunk, last_chunk) = ledger + // .first_and_last_stake_chunks() + // .ok_or(Error::::InternalClaimStakerError)?; + // let era_rewards = + // EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) + // .ok_or(Error::::NoClaimableRewards)?; + + // // The last era for which we can theoretically claim rewards. + // // And indicator if we know the period's ending era. + // let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { + // (protocol_state.era.saturating_sub(1), None) + // } else { + // PeriodEnd::::get(&staked_period) + // .map(|info| (info.final_era, Some(info.final_era))) + // .ok_or(Error::::InternalClaimStakerError)? + // }; + + // // The last era for which we can claim rewards for this account. + // // Limiting factors are: + // // 1. era reward span entries + // // 2. last era in period limit + // // 3. last era in which staker had non-zero stake + // let last_claim_era = if last_chunk.get_amount().is_zero() { + // era_rewards + // .last_era() + // .min(last_period_era) + // .min(last_chunk.get_era()) + // } else { + // era_rewards.last_era().min(last_period_era) + // }; + + // // Get chunks for reward claiming + // let chunks_for_claim = + // ledger + // .claim_up_to_era(last_claim_era, period_end) + // .map_err(|err| match err { + // AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, + // _ => Error::::InternalClaimStakerError, + // })?; + + // // Calculate rewards + // let mut rewards: Vec<_> = Vec::new(); + // let mut reward_sum = Balance::zero(); + // for era in first_chunk.get_era()..=last_claim_era { + // // TODO: this should be zipped, and values should be fetched only once + // let era_reward = era_rewards + // .get(era) + // .ok_or(Error::::InternalClaimStakerError)?; + + // let chunk = chunks_for_claim + // .get(era) + // .ok_or(Error::::InternalClaimStakerError)?; + + // // Optimization, and zero-division protection + // if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { + // continue; + // } + // let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) + // * era_reward.staker_reward_pool(); + + // rewards.push((era, staker_reward)); + // reward_sum.saturating_accrue(staker_reward); + // } + + // T::Currency::deposit_into_existing(&account, reward_sum) + // .map_err(|_| Error::::InternalClaimStakerError)?; + + // Self::update_ledger(&account, ledger); + + // rewards.into_iter().for_each(|(era, reward)| { + // Self::deposit_event(Event::::Reward { + // account: account.clone(), + // era, + // amount: reward, + // }); + // }); + + // Ok(()) + // } + } + + impl Pallet { + /// `Err` if pallet disabled for maintenance, `Ok` otherwise. + pub(crate) fn ensure_pallet_enabled() -> Result<(), Error> { + if ActiveProtocolState::::get().maintenance { + Err(Error::::Disabled) + } else { + Ok(()) + } + } + + /// Ensure that the origin is either the `ManagerOrigin` or a signed origin. + /// + /// In case of manager, `Ok(None)` is returned, and if signed origin `Ok(Some(AccountId))` is returned. + pub(crate) fn ensure_signed_or_manager( + origin: T::RuntimeOrigin, + ) -> Result, BadOrigin> { + if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() { + return Ok(None); + } + let who = ensure_signed(origin)?; + Ok(Some(who)) + } + + /// Update the account ledger, and dApp staking balance lock. + /// + /// In case account ledger is empty, entries from the DB are removed and lock is released. + pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { + if ledger.is_empty() { + Ledger::::remove(&account); + T::Currency::remove_lock(STAKING_ID, account); + } else { + T::Currency::set_lock( + STAKING_ID, + account, + ledger.active_locked_amount(), + WithdrawReasons::all(), + ); + Ledger::::insert(account, ledger); + } + } + + /// Returns the number of blocks per voting period. + pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { + T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) + } + + /// `true` if smart contract is active, `false` if it has been unregistered. + fn is_active(smart_contract: &T::SmartContract) -> bool { + IntegratedDApps::::get(smart_contract) + .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) + } + + /// Calculates the `EraRewardSpan` index for the specified era. + pub fn era_reward_span_index(era: EraNumber) -> EraNumber { + era.saturating_sub(era % T::EraRewardSpanLength::get()) + } + } +} diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index c3b15bdc1c..ab88c11773 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -39,215 +39,214 @@ pub(crate) type Balance = u128; pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; pub(crate) const MINIMUM_LOCK_AMOUNT: Balance = 10; -// type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -// type Block = frame_system::mocking::MockBlock; - -// construct_runtime!( -// pub struct Test -// where -// Block = Block, -// NodeBlock = Block, -// UncheckedExtrinsic = UncheckedExtrinsic, -// { -// System: frame_system, -// Balances: pallet_balances, -// DappStaking: pallet_dapp_staking, -// } -// ); - -// parameter_types! { -// pub const BlockHashCount: u64 = 250; -// pub BlockWeights: frame_system::limits::BlockWeights = -// frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); -// } - -// impl frame_system::Config for Test { -// type BaseCallFilter = frame_support::traits::Everything; -// type BlockWeights = (); -// type BlockLength = (); -// type RuntimeOrigin = RuntimeOrigin; -// type Index = u64; -// type RuntimeCall = RuntimeCall; -// type BlockNumber = BlockNumber; -// type Hash = H256; -// type Hashing = BlakeTwo256; -// type AccountId = AccountId; -// type Lookup = IdentityLookup; -// type Header = Header; -// type RuntimeEvent = RuntimeEvent; -// type BlockHashCount = BlockHashCount; -// type DbWeight = (); -// type Version = (); -// type PalletInfo = PalletInfo; -// type AccountData = pallet_balances::AccountData; -// type OnNewAccount = (); -// type OnKilledAccount = (); -// type SystemWeightInfo = (); -// type SS58Prefix = (); -// type OnSetCode = (); -// type MaxConsumers = frame_support::traits::ConstU32<16>; -// } - -// impl pallet_balances::Config for Test { -// type MaxLocks = ConstU32<4>; -// type MaxReserves = (); -// type ReserveIdentifier = [u8; 8]; -// type Balance = Balance; -// type RuntimeEvent = RuntimeEvent; -// type DustRemoval = (); -// type ExistentialDeposit = ConstU128; -// type AccountStore = System; -// type HoldIdentifier = (); -// type FreezeIdentifier = (); -// type MaxHolds = ConstU32<0>; -// type MaxFreezes = ConstU32<0>; -// type WeightInfo = (); -// } - -// impl pallet_dapp_staking::Config for Test { -// type RuntimeEvent = RuntimeEvent; -// type Currency = Balances; -// type SmartContract = MockSmartContract; -// type ManagerOrigin = frame_system::EnsureRoot; -// type StandardEraLength = ConstU64<10>; -// type StandardErasPerVotingPeriod = ConstU32<8>; -// type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; -// type EraRewardSpanLength = ConstU32<8>; -// type RewardRetentionInPeriods = ConstU32<2>; -// type MaxNumberOfContracts = ConstU16<10>; -// type MaxUnlockingChunks = ConstU32<5>; -// type MaxStakingChunks = ConstU32<8>; -// type MinimumLockedAmount = ConstU128; -// type UnlockingPeriod = ConstU64<20>; -// type MinimumStakeAmount = ConstU128<3>; -// } - -// // TODO: why not just change this to e.g. u32 for test? -// #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -// pub enum MockSmartContract { -// Wasm(AccountId), -// Other(AccountId), -// } - -// impl Default for MockSmartContract { -// fn default() -> Self { -// MockSmartContract::Wasm(1) -// } -// } - -// pub struct ExtBuilder; -// impl ExtBuilder { -// pub fn build() -> TestExternalities { -// let mut storage = frame_system::GenesisConfig::default() -// .build_storage::() -// .unwrap(); - -// let balances = vec![1000; 9] -// .into_iter() -// .enumerate() -// .map(|(idx, amount)| (idx as u64 + 1, amount)) -// .collect(); - -// pallet_balances::GenesisConfig:: { balances: balances } -// .assimilate_storage(&mut storage) -// .ok(); - -// let mut ext = TestExternalities::from(storage); -// ext.execute_with(|| { -// System::set_block_number(1); - -// // TODO: not sure why the mess with type happens here, I can check it later -// let era_length: BlockNumber = -// <::StandardEraLength as sp_core::Get<_>>::get(); -// let voting_period_length_in_eras: EraNumber = -// <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( -// ); - -// // TODO: handle this via GenesisConfig, and some helper functions to set the state -// pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { -// era: 1, -// next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, -// period_info: PeriodInfo { -// number: 1, -// period_type: PeriodType::Voting, -// ending_era: 2, -// }, -// maintenance: false, -// }); - -// // DappStaking::on_initialize(System::block_number()); -// }); - -// ext -// } -// } - -// /// Run to the specified block number. -// /// Function assumes first block has been initialized. -// pub(crate) fn run_to_block(n: u64) { -// while System::block_number() < n { -// DappStaking::on_finalize(System::block_number()); -// System::set_block_number(System::block_number() + 1); -// // This is performed outside of dapps staking but we expect it before on_initialize -// DappStaking::on_initialize(System::block_number()); -// } -// } - -// /// Run for the specified number of blocks. -// /// Function assumes first block has been initialized. -// pub(crate) fn run_for_blocks(n: u64) { -// run_to_block(System::block_number() + n); -// } - -// /// Advance blocks until the specified era has been reached. -// /// -// /// Function has no effect if era is already passed. -// pub(crate) fn advance_to_era(era: EraNumber) { -// assert!(era >= ActiveProtocolState::::get().era); -// while ActiveProtocolState::::get().era < era { -// run_for_blocks(1); -// } -// } - -// /// Advance blocks until next era has been reached. -// pub(crate) fn advance_to_next_era() { -// advance_to_era(ActiveProtocolState::::get().era + 1); -// } - -// /// Advance blocks until the specified period has been reached. -// /// -// /// Function has no effect if period is already passed. -// pub(crate) fn advance_to_period(period: PeriodNumber) { -// assert!(period >= ActiveProtocolState::::get().period_number()); -// while ActiveProtocolState::::get().period_number() < period { -// run_for_blocks(1); -// } -// } - -// /// Advance blocks until next period has been reached. -// pub(crate) fn advance_to_next_period() { -// advance_to_period(ActiveProtocolState::::get().period_number() + 1); -// } - -// /// Advance blocks until next period type has been reached. -// pub(crate) fn _advance_to_next_period_type() { -// let period_type = ActiveProtocolState::::get().period_type(); -// while ActiveProtocolState::::get().period_type() == period_type { -// run_for_blocks(1); -// } -// } - -// // Return all dApp staking events from the event buffer. -// pub fn dapp_staking_events() -> Vec> { -// System::events() -// .into_iter() -// .map(|r| r.event) -// .filter_map(|e| { -// if let RuntimeEvent::DappStaking(inner) = e { -// Some(inner) -// } else { -// None -// } -// }) -// .collect() -// } +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + DappStaking: pallet_dapp_staking, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128; + type AccountStore = System; + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; + type WeightInfo = (); +} + +impl pallet_dapp_staking::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type SmartContract = MockSmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type StandardEraLength = ConstU64<10>; + type StandardErasPerVotingPeriod = ConstU32<8>; + type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU16<10>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128; + type UnlockingPeriod = ConstU64<20>; + type MinimumStakeAmount = ConstU128<3>; +} + +// TODO: why not just change this to e.g. u32 for test? +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] +pub enum MockSmartContract { + Wasm(AccountId), + Other(AccountId), +} + +impl Default for MockSmartContract { + fn default() -> Self { + MockSmartContract::Wasm(1) + } +} + +pub struct ExtBuilder; +impl ExtBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let balances = vec![1000; 9] + .into_iter() + .enumerate() + .map(|(idx, amount)| (idx as u64 + 1, amount)) + .collect(); + + pallet_balances::GenesisConfig:: { balances: balances } + .assimilate_storage(&mut storage) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + System::set_block_number(1); + + // TODO: not sure why the mess with type happens here, I can check it later + let era_length: BlockNumber = + <::StandardEraLength as sp_core::Get<_>>::get(); + let voting_period_length_in_eras: EraNumber = + <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( + ); + + // TODO: handle this via GenesisConfig, and some helper functions to set the state + pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { + era: 1, + next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, + period_info: PeriodInfo { + number: 1, + period_type: PeriodType::Voting, + ending_era: 2, + }, + maintenance: false, + }); + + // DappStaking::on_initialize(System::block_number()); + }); + + ext + } +} + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +pub(crate) fn run_to_block(n: u64) { + while System::block_number() < n { + DappStaking::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + // This is performed outside of dapps staking but we expect it before on_initialize + DappStaking::on_initialize(System::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +pub(crate) fn run_for_blocks(n: u64) { + run_to_block(System::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(crate) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks(1); + } +} + +/// Advance blocks until next era has been reached. +pub(crate) fn advance_to_next_era() { + advance_to_era(ActiveProtocolState::::get().era + 1); +} + +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(crate) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks(1); + } +} + +/// Advance blocks until next period has been reached. +pub(crate) fn advance_to_next_period() { + advance_to_period(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period type has been reached. +pub(crate) fn _advance_to_next_period_type() { + let period_type = ActiveProtocolState::::get().period_type(); + while ActiveProtocolState::::get().period_type() == period_type { + run_for_blocks(1); + } +} + +// Return all dApp staking events from the event buffer. +pub fn dapp_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let RuntimeEvent::DappStaking(inner) = e { + Some(inner) + } else { + None + } + }) + .collect() +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index a9e6e336fe..5ad99cb1a2 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -292,12 +292,12 @@ fn account_ledger_stakeable_amount_works() { // Sanity check for empty ledger assert!(acc_ledger.stakeable_amount(1).is_zero()); - // First scenario - some locked amount, no staking chunks - let first_period = 1; + // 1st scenario - some locked amount, no staking chunks + let period_1 = 1; let locked_amount = 19; acc_ledger.add_lock_amount(locked_amount); assert_eq!( - acc_ledger.stakeable_amount(first_period), + acc_ledger.stakeable_amount(period_1), locked_amount, "Stakeable amount has to be equal to the locked amount" ); @@ -305,38 +305,42 @@ fn account_ledger_stakeable_amount_works() { // Second scenario - some staked amount is introduced, period is still valid let first_era = 1; let staked_amount = 7; - acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, first_period); + acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, period_1); assert_eq!( - acc_ledger.stakeable_amount(first_period), + acc_ledger.stakeable_amount(period_1), locked_amount - staked_amount, "Total stakeable amount should be equal to the locked amount minus what is already staked." ); // Third scenario - continuation of the previous, but we move to the next period. assert_eq!( - acc_ledger.stakeable_amount(first_period + 1), + acc_ledger.stakeable_amount(period_1 + 1), locked_amount, "Stakeable amount has to be equal to the locked amount since old period staking isn't valid anymore" ); } #[test] -fn account_ledger_add_stake_amount_works() { +fn account_ledger_add_stake_amount_basic_example_works() { get_u32_type!(UnlockingDummy, 5); let mut acc_ledger = AccountLedger::::default(); // Sanity check + let period_number = 2; assert!(acc_ledger - .add_stake_amount(0, 0, PeriodInfo::new(0, PeriodType::Voting, 0)) + .add_stake_amount(0, 0, PeriodInfo::new(period_number, PeriodType::Voting, 0)) .is_ok()); assert!(acc_ledger.staked.is_empty()); assert!(acc_ledger.staked_future.is_none()); + assert!(acc_ledger + .staked_amount_for_type(PeriodType::Voting, period_number) + .is_zero()); - // First scenario - stake some amount, and ensure values are as expected. + // 1st scenario - stake some amount, and ensure values are as expected. let first_era = 1; - let first_period = 1; - let period_info_1 = PeriodInfo::new(first_period, PeriodType::Voting, 100); + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); let lock_amount = 17; let stake_amount = 11; acc_ledger.add_lock_amount(lock_amount); @@ -354,17 +358,17 @@ fn account_ledger_add_stake_amount_works() { .staked_future .expect("Must exist after stake.") .period, - first_period + period_1 ); assert_eq!(acc_ledger.staked_future.unwrap().voting, stake_amount); assert!(acc_ledger.staked_future.unwrap().build_and_earn.is_zero()); - assert_eq!(acc_ledger.staked_amount(first_period), stake_amount); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, first_period), + acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), stake_amount ); assert!(acc_ledger - .staked_amount_for_type(PeriodType::BuildAndEarn, first_period) + .staked_amount_for_type(PeriodType::BuildAndEarn, period_1) .is_zero()); // Second scenario - stake some more to the same era @@ -372,19 +376,62 @@ fn account_ledger_add_stake_amount_works() { assert!(acc_ledger .add_stake_amount(1, first_era, period_info_1) .is_ok()); - assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); assert_eq!(acc_ledger.staked, snapshot); } #[test] -fn account_ledger_add_stake_amount_invalid_era_fails() { +fn account_ledger_add_stake_amount_advanced_example_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // 1st scenario - stake some amount, and ensure values are as expected. + let first_era = 1; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let lock_amount = 17; + let stake_amount_1 = 11; + acc_ledger.add_lock_amount(lock_amount); + + // We only have entry for the current era + acc_ledger.staked = StakeAmount::new(stake_amount_1, 0, first_era, period_1); + + let stake_amount_2 = 2; + let acc_ledger_snapshot = acc_ledger.clone(); + assert!(acc_ledger + .add_stake_amount(stake_amount_2, first_era, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + stake_amount_1 + stake_amount_2 + ); + assert_eq!( + acc_ledger.staked, acc_ledger_snapshot.staked, + "This entry must remain unchanged." + ); + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), + stake_amount_1 + stake_amount_2 + ); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::Voting), + stake_amount_1 + stake_amount_2 + ); + assert_eq!(acc_ledger.staked_future.unwrap().era, first_era + 1); +} + +#[test] +fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { get_u32_type!(UnlockingDummy, 5); let mut acc_ledger = AccountLedger::::default(); // Prep actions let first_era = 5; - let first_period = 2; - let period_info_1 = PeriodInfo::new(first_period, PeriodType::Voting, 100); + let period_1 = 2; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); let lock_amount = 13; let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); @@ -394,1153 +441,1064 @@ fn account_ledger_add_stake_amount_invalid_era_fails() { let acc_ledger_snapshot = acc_ledger.clone(); // Try to add to the next era, it should fail. - println!("{:?}", acc_ledger); assert_eq!( acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); + + // Try to add to the next period, it should fail. assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + ), + Err(AccountLedgerError::InvalidPeriod) ); - // Try to add to the previous era, it should fail. + // Alternative situation - no future entry, only current era + acc_ledger.staked = StakeAmount::new(0, stake_amount, first_era, period_1); + acc_ledger.staked_future = None; + assert_eq!( - acc_ledger.add_stake_amount(1, first_era - 1, period_info_1), + acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + ), + Err(AccountLedgerError::InvalidPeriod) + ); +} + +#[test] +fn account_ledger_add_stake_amount_too_large_amount_fails() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check + assert_eq!( + acc_ledger.add_stake_amount(10, 1, PeriodInfo::new(1, PeriodType::Voting, 100)), + Err(AccountLedgerError::UnavailableStakeFunds) + ); + + // Lock some amount, and try to stake more than that + let first_era = 5; + let period_1 = 2; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let lock_amount = 13; + acc_ledger.add_lock_amount(lock_amount); + assert_eq!( + acc_ledger.add_stake_amount(lock_amount + 1, first_era, period_info_1), + Err(AccountLedgerError::UnavailableStakeFunds) + ); + + // Additional check - have some active stake, and then try to overstake + assert!(acc_ledger + .add_stake_amount(lock_amount - 2, first_era, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.add_stake_amount(3, first_era, period_info_1), + Err(AccountLedgerError::UnavailableStakeFunds) + ); +} + +#[test] +fn account_ledger_unstake_amount_basic_scenario_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 19; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + + let mut acc_ledger_2 = acc_ledger.clone(); + + // 'Current' staked entry will remain empty. + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + // Only 'current' entry has some values, future is set to None. + acc_ledger_2.staked = StakeAmount::new(0, amount_1, era_1, period_1); + acc_ledger_2.staked_future = None; + + for mut acc_ledger in vec![acc_ledger, acc_ledger_2] { + // Sanity check + assert!(acc_ledger.unstake_amount(0, era_1, period_info_1).is_ok()); + + // 1st scenario - unstake some amount from the current era. + let unstake_amount_1 = 3; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 + ); + + // 2nd scenario - perform full unstake + assert!(acc_ledger + .unstake_amount(amount_1 - unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert!(acc_ledger.staked_amount(period_1).is_zero()); + assert!(acc_ledger.staked.is_empty()); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert!(acc_ledger.staked_future.is_none()); + } +} +#[test] +fn account_ledger_unstake_amount_advanced_scenario_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 19; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + + // We have two entries at once + acc_ledger.staked = StakeAmount::new(amount_1 - 1, 0, era_1, period_1); + acc_ledger.staked_future = Some(StakeAmount::new(amount_1 - 1, 1, era_1 + 1, period_1)); + + // 1st scenario - unstake some amount from the current era, both entries should be affected. + let unstake_amount_1 = 3; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 + ); + + assert_eq!( + acc_ledger.staked.for_type(PeriodType::Voting), + amount_1 - 1 - 3 + ); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::Voting), + amount_1 - 3 + ); + assert!(acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::BuildAndEarn) + .is_zero()); + + // 2nd scenario - perform full unstake + assert!(acc_ledger + .unstake_amount(amount_1 - unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert!(acc_ledger.staked_amount(period_1).is_zero()); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert!(acc_ledger.staked_future.is_none()); + + // 3rd scenario - try to stake again, ensure it works + let era_2 = era_1 + 7; + let amount_2 = amount_1 - 5; + assert!(acc_ledger + .add_stake_amount(amount_2, era_2, period_info_1) + .is_ok()); + assert_eq!(acc_ledger.staked_amount(period_1), amount_2); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::BuildAndEarn), + amount_2 + ); +} + +#[test] +fn account_ledger_unstake_from_invalid_era_fails() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 13; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + // Try to add to the next era, it should fail. + assert_eq!( + acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); // Try to add to the next period, it should fail. assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, - PeriodInfo::new(first_period + 1, PeriodType::Voting, 100) + era_1, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) ), Err(AccountLedgerError::InvalidPeriod) ); + + // Alternative situation - no future entry, only current era + acc_ledger.staked = StakeAmount::new(0, 1, era_1, period_1); + acc_ledger.staked_future = None; + assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); - - // Try to add to the previous period, it should fail assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, - PeriodInfo::new(first_period - 1, PeriodType::Voting, 100) + era_1, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) ), Err(AccountLedgerError::InvalidPeriod) ); +} + +#[test] +fn account_ledger_unstake_too_much_fails() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 23; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + assert_eq!( + acc_ledger.unstake_amount(amount_1 + 1, era_1, period_info_1), + Err(AccountLedgerError::UnstakeAmountLargerThanStake) + ); +} + +#[test] +fn account_ledger_unlockable_amount_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.unlockable_amount(0).is_zero()); + + // Nothing is staked + let lock_amount = 29; + let lock_era = 3; + acc_ledger.add_lock_amount(lock_amount); + assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); + + // Some amount is staked, period matches + let stake_period = 5; + let stake_amount = 17; + let period_info = PeriodInfo::new(stake_period, PeriodType::Voting, 100); + assert!(acc_ledger + .add_stake_amount(stake_amount, lock_era, period_info) + .is_ok()); + assert_eq!( + acc_ledger.unlockable_amount(stake_period), + lock_amount - stake_amount + ); + + // Period doesn't match + assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); + assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); + + // Absurd example, for the sake of completeness - staked without any lock + acc_ledger.locked = Balance::zero(); + assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); + assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); + assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); +} + +#[test] +fn account_ledger_claim_unlocked_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.claim_unlocked(0).is_zero()); + + // Add a chunk, assert it can be claimed correctly + let amount = 19; + let block_number = 1; + assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); + assert!(acc_ledger.claim_unlocked(0).is_zero()); + assert_eq!(acc_ledger.claim_unlocked(block_number), amount); + assert!(acc_ledger.unlocking.is_empty()); + + // Add multiple chunks, assert claim works correctly + let (amount1, amount2, amount3) = (7, 13, 19); + let (block1, block2, block3) = (1, 3, 5); + + // Prepare unlocking chunks + assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); + + // Only claim 1 chunk + assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); + assert_eq!(acc_ledger.unlocking.len(), 2); + + // Claim remaining two chunks + assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); + assert!(acc_ledger.unlocking.is_empty()); +} + +#[test] +fn account_ledger_consume_unlocking_chunks_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.consume_unlocking_chunks().is_zero()); + + // Add multiple chunks, cal should return correct amount + let (amount1, amount2) = (7, 13); + assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); + + assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); + assert!(acc_ledger.unlocking.is_empty()); +} + +#[test] +fn account_ledger_claim_up_to_era_works() { + // TODO!!! +} + +#[test] +fn era_info_lock_unlock_works() { + let mut era_info = EraInfo::default(); + + // Sanity check + assert!(era_info.total_locked.is_zero()); + assert!(era_info.active_era_locked.is_zero()); + assert!(era_info.unlocking.is_zero()); + + // Basic add lock + let lock_amount = 7; + era_info.add_locked(lock_amount); + assert_eq!(era_info.total_locked, lock_amount); + era_info.add_locked(lock_amount); + assert_eq!(era_info.total_locked, lock_amount * 2); + + // Basic unlocking started + let unlock_amount = 2; + era_info.total_locked = 17; + era_info.active_era_locked = 13; + let era_info_snapshot = era_info; + + // First unlock & checks + era_info.unlocking_started(unlock_amount); + assert_eq!( + era_info.total_locked, + era_info_snapshot.total_locked - unlock_amount + ); + assert_eq!( + era_info.active_era_locked, + era_info_snapshot.active_era_locked - unlock_amount + ); + assert_eq!(era_info.unlocking, unlock_amount); + + // Second unlock and checks + era_info.unlocking_started(unlock_amount); + assert_eq!( + era_info.total_locked, + era_info_snapshot.total_locked - unlock_amount * 2 + ); + assert_eq!( + era_info.active_era_locked, + era_info_snapshot.active_era_locked - unlock_amount * 2 + ); + assert_eq!(era_info.unlocking, unlock_amount * 2); + + // Claim unlocked chunks + let old_era_info = era_info.clone(); + era_info.unlocking_removed(1); + assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); + assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); +} + +#[test] +fn era_info_stake_works() { + let mut era_info = EraInfo::default(); + + // Sanity check + assert!(era_info.total_locked.is_zero()); + + // Add some voting period stake + let vp_stake_amount = 7; + era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); + assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::Voting), + vp_stake_amount + ); + assert!( + era_info.total_staked_amount().is_zero(), + "Calling stake makes it available only from the next era." + ); + + // Add some build&earn period stake + let bep_stake_amount = 13; + era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); + assert_eq!( + era_info.total_staked_amount_next_era(), + vp_stake_amount + bep_stake_amount + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + bep_stake_amount + ); + assert!( + era_info.total_staked_amount().is_zero(), + "Calling stake makes it available only from the next era." + ); +} + +#[test] +fn era_info_unstake_works() { + let mut era_info = EraInfo::default(); + + // Make dummy era info with stake amounts + let vp_stake_amount = 15; + let bep_stake_amount_1 = 23; + let bep_stake_amount_2 = bep_stake_amount_1 + 6; + let period_number = 1; + let era = 2; + era_info.current_stake_amount = + StakeAmount::new(vp_stake_amount, bep_stake_amount_1, era, period_number); + era_info.next_stake_amount = + StakeAmount::new(vp_stake_amount, bep_stake_amount_2, era + 1, period_number); + let total_staked = era_info.total_staked_amount(); + let total_staked_next_era = era_info.total_staked_amount_next_era(); + + // 1st scenario - unstake some amount, no overflow + let unstake_amount_1 = bep_stake_amount_1; + era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); + + // Current era + assert_eq!( + era_info.total_staked_amount(), + total_staked - unstake_amount_1 + ); + assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); + assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); + + // Next era + assert_eq!( + era_info.total_staked_amount_next_era(), + total_staked_next_era - unstake_amount_1 + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::Voting), + vp_stake_amount + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + bep_stake_amount_2 - unstake_amount_1 + ); + + // 2nd scenario - unstake some more, but with overflow + let overflow = 2; + let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; + era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); + + // Current era + assert_eq!( + era_info.total_staked_amount(), + total_staked - unstake_amount_1 - unstake_amount_2 + ); + + // Next era + assert_eq!( + era_info.total_staked_amount_next_era(), + vp_stake_amount - overflow + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::Voting), + vp_stake_amount - overflow + ); + assert!(era_info + .staked_amount_next_era(PeriodType::BuildAndEarn) + .is_zero()); +} + +#[test] +fn stake_amount_works() { + let mut stake_amount = StakeAmount::default(); + + // Sanity check + assert!(stake_amount.total().is_zero()); + assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + + // Stake some amount in voting period + let vp_stake_1 = 11; + stake_amount.add(vp_stake_1, PeriodType::Voting); + assert_eq!(stake_amount.total(), vp_stake_1); + assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + + // Stake some amount in build&earn period + let bep_stake_1 = 13; + stake_amount.add(bep_stake_1, PeriodType::BuildAndEarn); + assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); + assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); + assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + + // Unstake some amount from voting period + let vp_unstake_1 = 5; + stake_amount.subtract(5, PeriodType::Voting); + assert_eq!( + stake_amount.total(), + vp_stake_1 + bep_stake_1 - vp_unstake_1 + ); + assert_eq!( + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 + ); + assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + + // Unstake some amount from build&earn period + let bep_unstake_1 = 2; + stake_amount.subtract(bep_unstake_1, PeriodType::BuildAndEarn); + assert_eq!( + stake_amount.total(), + vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 + ); + assert_eq!( + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 + ); + assert_eq!( + stake_amount.for_type(PeriodType::BuildAndEarn), + bep_stake_1 - bep_unstake_1 + ); + + // Unstake some more from build&earn period, and chip away from the voting period + let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; + let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; + stake_amount.subtract(bep_unstake_2, PeriodType::BuildAndEarn); + assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 - 1 ); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); } -// #[test] -// fn account_ledger_add_stake_amount_too_large_amount_fails() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Sanity check -// assert_eq!( -// acc_ledger.add_stake_amount(10, 1, 1), -// Err(AccountLedgerError::UnavailableStakeFunds) -// ); - -// // Lock some amount, and try to stake more than that -// let first_era = 5; -// let first_period = 2; -// let lock_amount = 13; -// acc_ledger.add_lock_amount(lock_amount); -// assert_eq!( -// acc_ledger.add_stake_amount(lock_amount + 1, first_era, first_period), -// Err(AccountLedgerError::UnavailableStakeFunds) -// ); - -// // Additional check - have some active stake, and then try to overstake -// assert!(acc_ledger -// .add_stake_amount(lock_amount - 2, first_era, first_period) -// .is_ok()); -// assert_eq!( -// acc_ledger.add_stake_amount(3, first_era, first_period), -// Err(AccountLedgerError::UnavailableStakeFunds) -// ); -// } - -// #[test] -// fn account_ledger_add_stake_amount_while_exceeding_capacity_fails() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Try to stake up to the capacity, it should work -// // Lock some amount, and try to stake more than that -// let first_era = 5; -// let first_period = 2; -// let lock_amount = 31; -// let stake_amount = 3; -// acc_ledger.add_lock_amount(lock_amount); -// for inc in 0..StakingDummy::get() { -// assert!(acc_ledger -// .add_stake_amount(stake_amount, first_era + inc, first_period) -// .is_ok()); -// assert_eq!( -// acc_ledger.staked_amount(first_period), -// stake_amount * (inc as u128 + 1) -// ); -// } - -// // Can still stake to the last staked era -// assert!(acc_ledger -// .add_stake_amount( -// stake_amount, -// first_era + StakingDummy::get() - 1, -// first_period -// ) -// .is_ok()); - -// // But staking to the next era must fail with exceeded capacity -// assert_eq!( -// acc_ledger.add_stake_amount(stake_amount, first_era + StakingDummy::get(), first_period), -// Err(AccountLedgerError::NoCapacity) -// ); -// } - -// #[test] -// fn account_ledger_unstake_amount_works() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Prep actions -// let amount_1 = 19; -// let era_1 = 2; -// let period_1 = 1; -// acc_ledger.add_lock_amount(amount_1); -// assert!(acc_ledger -// .add_stake_amount(amount_1, era_1, period_1) -// .is_ok()); - -// // Sanity check -// assert!(acc_ledger.unstake_amount(0, era_1, period_1).is_ok()); - -// // 1st scenario - unstake some amount from the current era. -// let unstake_amount_1 = 3; -// assert!(acc_ledger -// .unstake_amount(unstake_amount_1, era_1, period_1) -// .is_ok()); -// assert_eq!( -// acc_ledger.staked_amount(period_1), -// amount_1 - unstake_amount_1 -// ); -// assert_eq!( -// acc_ledger.staked.0.len(), -// 1, -// "Only existing entry should be updated." -// ); - -// // 2nd scenario - unstake some more, but from the next era -// let era_2 = era_1 + 1; -// assert!(acc_ledger -// .unstake_amount(unstake_amount_1, era_2, period_1) -// .is_ok()); -// assert_eq!( -// acc_ledger.staked_amount(period_1), -// amount_1 - unstake_amount_1 * 2 -// ); -// assert_eq!( -// acc_ledger.staked.0.len(), -// 2, -// "New entry must be created to cover the new era stake." -// ); - -// // 3rd scenario - unstake some more, bump era by a larger number -// let era_3 = era_2 + 3; -// assert!(acc_ledger -// .unstake_amount(unstake_amount_1, era_3, period_1) -// .is_ok()); -// assert_eq!( -// acc_ledger.staked_amount(period_1), -// amount_1 - unstake_amount_1 * 3 -// ); -// assert_eq!( -// acc_ledger.staked.0.len(), -// 3, -// "New entry must be created to cover the new era stake." -// ); -// } - -// #[test] -// fn account_ledger_unstake_from_invalid_era_fails() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Prep actions -// let amount_1 = 13; -// let era_1 = 2; -// let period_1 = 1; -// acc_ledger.add_lock_amount(amount_1); -// assert!(acc_ledger -// .add_stake_amount(amount_1, era_1, period_1) -// .is_ok()); - -// assert_eq!( -// acc_ledger.unstake_amount(amount_1, era_1 + 1, period_1 + 1), -// Err(AccountLedgerError::InvalidPeriod) -// ); -// } - -// #[test] -// fn account_ledger_unstake_too_much_fails() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Prep actions -// let amount_1 = 23; -// let era_1 = 2; -// let period_1 = 1; -// acc_ledger.add_lock_amount(amount_1); -// assert!(acc_ledger -// .add_stake_amount(amount_1, era_1, period_1) -// .is_ok()); - -// assert_eq!( -// acc_ledger.unstake_amount(amount_1 + 1, era_1, period_1), -// Err(AccountLedgerError::UnstakeAmountLargerThanStake) -// ); -// } - -// #[test] -// fn account_ledger_unstake_exceeds_capacity() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Prep actions -// let amount_1 = 100; -// let era_1 = 2; -// let period_1 = 1; -// acc_ledger.add_lock_amount(amount_1); -// assert!(acc_ledger -// .add_stake_amount(amount_1, era_1, period_1) -// .is_ok()); - -// for x in 0..StakingDummy::get() { -// assert!( -// acc_ledger.unstake_amount(3, era_1 + x, period_1).is_ok(), -// "Capacity isn't full so unstake must work." -// ); -// } - -// assert_eq!( -// acc_ledger.unstake_amount(3, era_1 + StakingDummy::get(), period_1), -// Err(AccountLedgerError::NoCapacity) -// ); -// } - -// #[test] -// fn account_ledger_unlockable_amount_works() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Sanity check scenario -// assert!(acc_ledger.unlockable_amount(0).is_zero()); - -// // Nothing is staked -// let lock_amount = 29; -// let lock_era = 3; -// acc_ledger.add_lock_amount(lock_amount); -// assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); - -// // Some amount is staked, period matches -// let stake_period = 5; -// let stake_amount = 17; -// acc_ledger.staked = SparseBoundedAmountEraVec( -// BoundedVec::try_from(vec![StakeChunk { -// amount: stake_amount, -// era: lock_era, -// }]) -// .expect("Only one chunk so creation should succeed."), -// ); -// acc_ledger.staked_period = Some(stake_period); -// assert_eq!( -// acc_ledger.unlockable_amount(stake_period), -// lock_amount - stake_amount -// ); - -// // Period doesn't match -// assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); -// assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); - -// // Absurd example, for the sake of completeness - staked without any lock -// acc_ledger.locked = Balance::zero(); -// assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); -// assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); -// assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); -// } - -// #[test] -// fn account_ledger_claim_unlocked_works() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Sanity check scenario -// assert!(acc_ledger.claim_unlocked(0).is_zero()); - -// // Add a chunk, assert it can be claimed correctly -// let amount = 19; -// let block_number = 1; -// assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); -// assert!(acc_ledger.claim_unlocked(0).is_zero()); -// assert_eq!(acc_ledger.claim_unlocked(block_number), amount); -// assert!(acc_ledger.unlocking.is_empty()); - -// // Add multiple chunks, assert claim works correctly -// let (amount1, amount2, amount3) = (7, 13, 19); -// let (block1, block2, block3) = (1, 3, 5); - -// // Prepare unlocking chunks -// assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); -// assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); -// assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); - -// // Only claim 1 chunk -// assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); -// assert_eq!(acc_ledger.unlocking.len(), 2); - -// // Claim remaining two chunks -// assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); -// assert!(acc_ledger.unlocking.is_empty()); -// } - -// #[test] -// fn account_ledger_consume_unlocking_chunks_works() { -// get_u32_type!(UnlockingDummy, 5); -// get_u32_type!(StakingDummy, 8); -// let mut acc_ledger = AccountLedger::::default(); - -// // Sanity check scenario -// assert!(acc_ledger.consume_unlocking_chunks().is_zero()); - -// // Add multiple chunks, cal should return correct amount -// let (amount1, amount2) = (7, 13); -// assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); -// assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); - -// assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); -// assert!(acc_ledger.unlocking.is_empty()); -// } - -// #[test] -// fn era_info_lock_unlock_works() { -// let mut era_info = EraInfo::default(); - -// // Sanity check -// assert!(era_info.total_locked.is_zero()); -// assert!(era_info.active_era_locked.is_zero()); -// assert!(era_info.unlocking.is_zero()); - -// // Basic add lock -// let lock_amount = 7; -// era_info.add_locked(lock_amount); -// assert_eq!(era_info.total_locked, lock_amount); -// era_info.add_locked(lock_amount); -// assert_eq!(era_info.total_locked, lock_amount * 2); - -// // Basic unlocking started -// let unlock_amount = 2; -// era_info.total_locked = 17; -// era_info.active_era_locked = 13; -// let era_info_snapshot = era_info; - -// // First unlock & checks -// era_info.unlocking_started(unlock_amount); -// assert_eq!( -// era_info.total_locked, -// era_info_snapshot.total_locked - unlock_amount -// ); -// assert_eq!( -// era_info.active_era_locked, -// era_info_snapshot.active_era_locked - unlock_amount -// ); -// assert_eq!(era_info.unlocking, unlock_amount); - -// // Second unlock and checks -// era_info.unlocking_started(unlock_amount); -// assert_eq!( -// era_info.total_locked, -// era_info_snapshot.total_locked - unlock_amount * 2 -// ); -// assert_eq!( -// era_info.active_era_locked, -// era_info_snapshot.active_era_locked - unlock_amount * 2 -// ); -// assert_eq!(era_info.unlocking, unlock_amount * 2); - -// // Claim unlocked chunks -// let old_era_info = era_info.clone(); -// era_info.unlocking_removed(1); -// assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); -// assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); -// } - -// #[test] -// fn era_info_stake_works() { -// let mut era_info = EraInfo::default(); - -// // Sanity check -// assert!(era_info.total_locked.is_zero()); - -// // Add some voting period stake -// let vp_stake_amount = 7; -// era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); -// assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); -// assert_eq!( -// era_info.staked_amount_next_era(PeriodType::Voting), -// vp_stake_amount -// ); -// assert!( -// era_info.total_staked_amount().is_zero(), -// "Calling stake makes it available only from the next era." -// ); - -// // Add some build&earn period stake -// let bep_stake_amount = 13; -// era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); -// assert_eq!( -// era_info.total_staked_amount_next_era(), -// vp_stake_amount + bep_stake_amount -// ); -// assert_eq!( -// era_info.staked_amount_next_era(PeriodType::BuildAndEarn), -// bep_stake_amount -// ); -// assert!( -// era_info.total_staked_amount().is_zero(), -// "Calling stake makes it available only from the next era." -// ); -// } - -// #[test] -// fn era_info_unstake_works() { -// let mut era_info = EraInfo::default(); - -// // Make dummy era info with stake amounts -// let vp_stake_amount = 15; -// let bep_stake_amount_1 = 23; -// let bep_stake_amount_2 = bep_stake_amount_1 + 6; -// era_info.current_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_1); -// era_info.next_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_2); -// let total_staked = era_info.total_staked_amount(); -// let total_staked_next_era = era_info.total_staked_amount_next_era(); - -// // 1st scenario - unstake some amount, no overflow -// let unstake_amount_1 = bep_stake_amount_1; -// era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); - -// // Current era -// assert_eq!( -// era_info.total_staked_amount(), -// total_staked - unstake_amount_1 -// ); -// assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); -// assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); - -// // Next era -// assert_eq!( -// era_info.total_staked_amount_next_era(), -// total_staked_next_era - unstake_amount_1 -// ); -// assert_eq!( -// era_info.staked_amount_next_era(PeriodType::Voting), -// vp_stake_amount -// ); -// assert_eq!( -// era_info.staked_amount_next_era(PeriodType::BuildAndEarn), -// bep_stake_amount_2 - unstake_amount_1 -// ); - -// // 2nd scenario - unstake some more, but with overflow -// let overflow = 2; -// let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; -// era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); - -// // Current era -// assert_eq!( -// era_info.total_staked_amount(), -// total_staked - unstake_amount_1 - unstake_amount_2 -// ); - -// // Next era -// assert_eq!( -// era_info.total_staked_amount_next_era(), -// vp_stake_amount - overflow -// ); -// assert_eq!( -// era_info.staked_amount_next_era(PeriodType::Voting), -// vp_stake_amount - overflow -// ); -// assert!(era_info -// .staked_amount_next_era(PeriodType::BuildAndEarn) -// .is_zero()); -// } - -// #[test] -// fn stake_amount_works() { -// let mut stake_amount = StakeAmount::default(); - -// // Sanity check -// assert!(stake_amount.total().is_zero()); -// assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); -// assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); - -// // Stake some amount in voting period -// let vp_stake_1 = 11; -// stake_amount.stake(vp_stake_1, PeriodType::Voting); -// assert_eq!(stake_amount.total(), vp_stake_1); -// assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); -// assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); - -// // Stake some amount in build&earn period -// let bep_stake_1 = 13; -// stake_amount.stake(bep_stake_1, PeriodType::BuildAndEarn); -// assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); -// assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); -// assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); - -// // Unstake some amount from voting period -// let vp_unstake_1 = 5; -// stake_amount.unstake(5, PeriodType::Voting); -// assert_eq!( -// stake_amount.total(), -// vp_stake_1 + bep_stake_1 - vp_unstake_1 -// ); -// assert_eq!( -// stake_amount.for_type(PeriodType::Voting), -// vp_stake_1 - vp_unstake_1 -// ); -// assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); - -// // Unstake some amount from build&earn period -// let bep_unstake_1 = 2; -// stake_amount.unstake(bep_unstake_1, PeriodType::BuildAndEarn); -// assert_eq!( -// stake_amount.total(), -// vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 -// ); -// assert_eq!( -// stake_amount.for_type(PeriodType::Voting), -// vp_stake_1 - vp_unstake_1 -// ); -// assert_eq!( -// stake_amount.for_type(PeriodType::BuildAndEarn), -// bep_stake_1 - bep_unstake_1 -// ); - -// // Unstake some more from build&earn period, and chip away from the voting period -// let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; -// let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; -// stake_amount.unstake(bep_unstake_2, PeriodType::BuildAndEarn); -// assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); -// assert_eq!( -// stake_amount.for_type(PeriodType::Voting), -// vp_stake_1 - vp_unstake_1 - 1 -// ); -// assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); -// } - -// #[test] -// fn singular_staking_info_basics_are_ok() { -// let period_number = 3; -// let period_type = PeriodType::Voting; -// let mut staking_info = SingularStakingInfo::new(period_number, period_type); - -// // Sanity checks -// assert_eq!(staking_info.period_number(), period_number); -// assert!(staking_info.is_loyal()); -// assert!(staking_info.total_staked_amount().is_zero()); -// assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); - -// // Add some staked amount during `Voting` period -// let vote_stake_amount_1 = 11; -// staking_info.stake(vote_stake_amount_1, PeriodType::Voting); -// assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); -// assert_eq!( -// staking_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 -// ); -// assert!(staking_info -// .staked_amount(PeriodType::BuildAndEarn) -// .is_zero()); - -// // Add some staked amount during `BuildAndEarn` period -// let bep_stake_amount_1 = 23; -// staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); -// assert_eq!( -// staking_info.total_staked_amount(), -// vote_stake_amount_1 + bep_stake_amount_1 -// ); -// assert_eq!( -// staking_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 -// ); -// assert_eq!( -// staking_info.staked_amount(PeriodType::BuildAndEarn), -// bep_stake_amount_1 -// ); -// } - -// #[test] -// fn singular_staking_info_unstake_during_voting_is_ok() { -// let period_number = 3; -// let period_type = PeriodType::Voting; -// let mut staking_info = SingularStakingInfo::new(period_number, period_type); - -// // Prep actions -// let vote_stake_amount_1 = 11; -// staking_info.stake(vote_stake_amount_1, PeriodType::Voting); - -// // Unstake some amount during `Voting` period, loyalty should remain as expected. -// let unstake_amount_1 = 5; -// assert_eq!( -// staking_info.unstake(unstake_amount_1, PeriodType::Voting), -// (unstake_amount_1, Balance::zero()) -// ); -// assert_eq!( -// staking_info.total_staked_amount(), -// vote_stake_amount_1 - unstake_amount_1 -// ); -// assert!(staking_info.is_loyal()); - -// // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. -// let remaining_stake = staking_info.total_staked_amount(); -// assert_eq!( -// staking_info.unstake(remaining_stake + 1, PeriodType::Voting), -// (remaining_stake, Balance::zero()) -// ); -// assert!(staking_info.total_staked_amount().is_zero()); -// assert!(staking_info.is_loyal()); -// } - -// #[test] -// fn singular_staking_info_unstake_during_bep_is_ok() { -// let period_number = 3; -// let period_type = PeriodType::Voting; -// let mut staking_info = SingularStakingInfo::new(period_number, period_type); - -// // Prep actions -// let vote_stake_amount_1 = 11; -// staking_info.stake(vote_stake_amount_1, PeriodType::Voting); -// let bep_stake_amount_1 = 23; -// staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); - -// // 1st scenario - Unstake some of the amount staked during B&E period -// let unstake_1 = 5; -// assert_eq!( -// staking_info.unstake(5, PeriodType::BuildAndEarn), -// (Balance::zero(), unstake_1) -// ); -// assert_eq!( -// staking_info.total_staked_amount(), -// vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 -// ); -// assert_eq!( -// staking_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 -// ); -// assert_eq!( -// staking_info.staked_amount(PeriodType::BuildAndEarn), -// bep_stake_amount_1 - unstake_1 -// ); -// assert!(staking_info.is_loyal()); - -// // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. -// // The point is to take a chunk from the voting period stake too. -// let current_total_stake = staking_info.total_staked_amount(); -// let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); -// let voting_stake_overflow = 2; -// let unstake_2 = current_bep_stake + voting_stake_overflow; - -// assert_eq!( -// staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), -// (voting_stake_overflow, current_bep_stake) -// ); -// assert_eq!( -// staking_info.total_staked_amount(), -// current_total_stake - unstake_2 -// ); -// assert_eq!( -// staking_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 - voting_stake_overflow -// ); -// assert!(staking_info -// .staked_amount(PeriodType::BuildAndEarn) -// .is_zero()); -// assert!( -// !staking_info.is_loyal(), -// "Loyalty flag should have been removed due to non-zero voting period unstake" -// ); -// } - -// #[test] -// fn contract_stake_info_is_ok() { -// let period = 2; -// let era = 3; -// let mut contract_stake_info = ContractStakingInfo::new(era, period); - -// // Sanity check -// assert_eq!(contract_stake_info.period(), period); -// assert_eq!(contract_stake_info.era(), era); -// assert!(contract_stake_info.total_staked_amount().is_zero()); -// assert!(contract_stake_info.is_empty()); - -// // 1st scenario - Add some staked amount to the voting period -// let vote_stake_amount_1 = 11; -// contract_stake_info.stake(vote_stake_amount_1, PeriodType::Voting); -// assert_eq!( -// contract_stake_info.total_staked_amount(), -// vote_stake_amount_1 -// ); -// assert_eq!( -// contract_stake_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 -// ); -// assert!(!contract_stake_info.is_empty()); - -// // 2nd scenario - add some staked amount to the B&E period -// let bep_stake_amount_1 = 23; -// contract_stake_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); -// assert_eq!( -// contract_stake_info.total_staked_amount(), -// vote_stake_amount_1 + bep_stake_amount_1 -// ); -// assert_eq!( -// contract_stake_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 -// ); -// assert_eq!( -// contract_stake_info.staked_amount(PeriodType::BuildAndEarn), -// bep_stake_amount_1 -// ); - -// // 3rd scenario - reduce some of the staked amount from both periods and verify it's as expected. -// let total_staked = contract_stake_info.total_staked_amount(); -// let vp_reduction = 3; -// contract_stake_info.unstake(vp_reduction, PeriodType::Voting); -// assert_eq!( -// contract_stake_info.total_staked_amount(), -// total_staked - vp_reduction -// ); -// assert_eq!( -// contract_stake_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 - vp_reduction -// ); - -// let bp_reduction = 7; -// contract_stake_info.unstake(bp_reduction, PeriodType::BuildAndEarn); -// assert_eq!( -// contract_stake_info.total_staked_amount(), -// total_staked - vp_reduction - bp_reduction -// ); -// assert_eq!( -// contract_stake_info.staked_amount(PeriodType::BuildAndEarn), -// bep_stake_amount_1 - bp_reduction -// ); - -// // 4th scenario - unstake everything, and some more, from Build&Earn period, chiping away from the voting period. -// let overflow = 1; -// let overflow_reduction = contract_stake_info.staked_amount(PeriodType::BuildAndEarn) + overflow; -// contract_stake_info.unstake(overflow_reduction, PeriodType::BuildAndEarn); -// assert_eq!( -// contract_stake_info.total_staked_amount(), -// vote_stake_amount_1 - vp_reduction - overflow -// ); -// assert!(contract_stake_info -// .staked_amount(PeriodType::BuildAndEarn) -// .is_zero()); -// assert_eq!( -// contract_stake_info.staked_amount(PeriodType::Voting), -// vote_stake_amount_1 - vp_reduction - overflow -// ); -// } - -// #[test] -// fn contract_staking_info_series_get_works() { -// let info_1 = ContractStakingInfo::new(4, 2); -// let mut info_2 = ContractStakingInfo::new(7, 3); -// info_2.stake(11, PeriodType::Voting); -// let mut info_3 = ContractStakingInfo::new(9, 3); -// info_3.stake(13, PeriodType::BuildAndEarn); - -// let series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); - -// // Sanity check -// assert_eq!(series.len(), 3); -// assert!(!series.is_empty()); - -// // 1st scenario - get existing entries -// assert_eq!(series.get(4, 2), Some(info_1)); -// assert_eq!(series.get(7, 3), Some(info_2)); -// assert_eq!(series.get(9, 3), Some(info_3)); - -// // 2nd scenario - get non-existing entries for covered eras -// { -// let era_1 = 6; -// let entry_1 = series.get(era_1, 2).expect("Has to be Some"); -// assert!(entry_1.total_staked_amount().is_zero()); -// assert_eq!(entry_1.era(), era_1); -// assert_eq!(entry_1.period(), 2); - -// let era_2 = 8; -// let entry_1 = series.get(era_2, 3).expect("Has to be Some"); -// assert_eq!(entry_1.total_staked_amount(), 11); -// assert_eq!(entry_1.era(), era_2); -// assert_eq!(entry_1.period(), 3); -// } - -// // 3rd scenario - get non-existing entries for covered eras but mismatching period -// assert!(series.get(8, 2).is_none()); - -// // 4th scenario - get non-existing entries for non-covered eras -// assert!(series.get(3, 2).is_none()); -// } - -// #[test] -// fn contract_staking_info_series_stake_is_ok() { -// let mut series = ContractStakingInfoSeries::default(); - -// // Sanity check -// assert!(series.is_empty()); -// assert!(series.len().is_zero()); - -// // 1st scenario - stake some amount and verify state change -// let era_1 = 3; -// let period_1 = 5; -// let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); -// let amount_1 = 31; -// assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); - -// assert_eq!(series.len(), 1); -// assert!(!series.is_empty()); - -// let entry_1_1 = series.get(era_1, period_1).unwrap(); -// assert_eq!(entry_1_1.era(), era_1); -// assert_eq!(entry_1_1.total_staked_amount(), amount_1); - -// // 2nd scenario - stake some more to the same era but different period type, and verify state change. -// let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); -// assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); -// assert_eq!( -// series.len(), -// 1, -// "No new entry should be created since it's the same era." -// ); -// let entry_1_2 = series.get(era_1, period_1).unwrap(); -// assert_eq!(entry_1_2.era(), era_1); -// assert_eq!(entry_1_2.total_staked_amount(), amount_1 * 2); - -// // 3rd scenario - stake more to the next era, while still in the same period. -// let era_2 = era_1 + 2; -// let amount_2 = 37; -// assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); -// assert_eq!(series.len(), 2); -// let entry_2_1 = series.get(era_1, period_1).unwrap(); -// let entry_2_2 = series.get(era_2, period_1).unwrap(); -// assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); -// assert_eq!(entry_2_2.era(), era_2); -// assert_eq!(entry_2_2.period(), period_1); -// assert_eq!( -// entry_2_2.total_staked_amount(), -// entry_2_1.total_staked_amount() + amount_2, -// "Since it's the same period, stake amount must carry over from the previous entry." -// ); - -// // 4th scenario - stake some more to the next era, but this time also bump the period. -// let era_3 = era_2 + 3; -// let period_2 = period_1 + 1; -// let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); -// let amount_3 = 41; - -// assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); -// assert_eq!(series.len(), 3); -// let entry_3_1 = series.get(era_1, period_1).unwrap(); -// let entry_3_2 = series.get(era_2, period_1).unwrap(); -// let entry_3_3 = series.get(era_3, period_2).unwrap(); -// assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); -// assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); -// assert_eq!(entry_3_3.era(), era_3); -// assert_eq!(entry_3_3.period(), period_2); -// assert_eq!( -// entry_3_3.total_staked_amount(), -// amount_3, -// "No carry over from previous entry since period has changed." -// ); - -// // 5th scenario - stake to the next era, expect cleanup of oldest entry -// let era_4 = era_3 + 1; -// let amount_4 = 5; -// assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); -// assert_eq!(series.len(), 3); -// let entry_4_1 = series.get(era_2, period_1).unwrap(); -// let entry_4_2 = series.get(era_3, period_2).unwrap(); -// let entry_4_3 = series.get(era_4, period_2).unwrap(); -// assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); -// assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); -// assert_eq!(entry_4_3.era(), era_4); -// assert_eq!(entry_4_3.period(), period_2); -// assert_eq!(entry_4_3.total_staked_amount(), amount_3 + amount_4); -// } - -// #[test] -// fn contract_staking_info_series_stake_with_inconsistent_data_fails() { -// let mut series = ContractStakingInfoSeries::default(); - -// // Create an entry with some staked amount -// let era = 5; -// let period_info = PeriodInfo { -// number: 7, -// period_type: PeriodType::Voting, -// ending_era: 31, -// }; -// let amount = 37; -// assert!(series.stake(amount, period_info, era).is_ok()); - -// // 1st scenario - attempt to stake using old era -// assert!(series.stake(amount, period_info, era - 1).is_err()); - -// // 2nd scenario - attempt to stake using old period -// let period_info = PeriodInfo { -// number: period_info.number - 1, -// period_type: PeriodType::Voting, -// ending_era: 31, -// }; -// assert!(series.stake(amount, period_info, era).is_err()); -// } - -// #[test] -// fn contract_staking_info_series_unstake_is_ok() { -// let mut series = ContractStakingInfoSeries::default(); - -// // Prep action - create a stake entry -// let era_1 = 2; -// let period = 3; -// let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); -// let stake_amount = 100; -// assert!(series.stake(stake_amount, period_info, era_1).is_ok()); - -// // 1st scenario - unstake in the same era -// let amount_1 = 5; -// assert!(series.unstake(amount_1, period_info, era_1).is_ok()); -// assert_eq!(series.len(), 1); -// assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); -// assert_eq!( -// series.staked_amount(period, PeriodType::Voting), -// stake_amount - amount_1 -// ); - -// // 2nd scenario - unstake in the future era, creating a 'gap' in the series -// // [(era: 2)] ---> [(era: 2), (era: 5)] -// let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); -// let era_2 = era_1 + 3; -// let amount_2 = 7; -// assert!(series.unstake(amount_2, period_info, era_2).is_ok()); -// assert_eq!(series.len(), 2); -// assert_eq!( -// series.total_staked_amount(period), -// stake_amount - amount_1 - amount_2 -// ); -// assert_eq!( -// series.staked_amount(period, PeriodType::Voting), -// stake_amount - amount_1 - amount_2 -// ); - -// // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones -// // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] -// let era_3 = era_2 - 1; -// let amount_3 = 11; -// assert!(series.unstake(amount_3, period_info, era_3).is_ok()); -// assert_eq!(series.len(), 3); -// assert_eq!( -// series.total_staked_amount(period), -// stake_amount - amount_1 - amount_2 - amount_3 -// ); -// assert_eq!( -// series.staked_amount(period, PeriodType::Voting), -// stake_amount - amount_1 - amount_2 - amount_3 -// ); - -// // Check concrete entries -// assert_eq!( -// series.get(era_1, period).unwrap().total_staked_amount(), -// stake_amount - amount_1, -// "Oldest entry must remain unchanged." -// ); -// assert_eq!( -// series.get(era_2, period).unwrap().total_staked_amount(), -// stake_amount - amount_1 - amount_2 - amount_3, -// "Future era entry must be updated with all of the reductions." -// ); -// assert_eq!( -// series.get(era_3, period).unwrap().total_staked_amount(), -// stake_amount - amount_1 - amount_3, -// "Second to last era entry must be updated with first & last reduction\ -// because it derives its initial value from the oldest entry." -// ); -// } - -// #[test] -// fn contract_staking_info_unstake_with_worst_case_scenario_for_capacity_overflow() { -// let (era_1, era_2, era_3) = (4, 7, 9); -// let (period_1, period_2) = (2, 3); -// let info_1 = ContractStakingInfo::new(era_1, period_1); -// let mut info_2 = ContractStakingInfo::new(era_2, period_2); -// let stake_amount_2 = 11; -// info_2.stake(stake_amount_2, PeriodType::Voting); -// let mut info_3 = ContractStakingInfo::new(era_3, period_2); -// let stake_amount_3 = 13; -// info_3.stake(stake_amount_3, PeriodType::BuildAndEarn); - -// // A gap between 2nd and 3rd era, and from that gap unstake will be done. -// // This will force a new entry to be created, potentially overflowing the vector capacity. -// let mut series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); - -// // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity -// let period_info = PeriodInfo { -// number: period_2, -// period_type: PeriodType::BuildAndEarn, -// ending_era: 51, -// }; -// let unstake_amount = 3; -// assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); -// assert_eq!(series.len(), 3); - -// assert_eq!( -// series.get(era_1, period_1), -// None, -// "Oldest entry should have been prunned" -// ); -// assert_eq!( -// series -// .get(era_2, period_2) -// .expect("Entry must exist.") -// .total_staked_amount(), -// stake_amount_2 -// ); -// assert_eq!( -// series -// .get(era_2 + 1, period_2) -// .expect("Entry must exist.") -// .total_staked_amount(), -// stake_amount_2 - unstake_amount -// ); -// assert_eq!( -// series -// .get(era_3, period_2) -// .expect("Entry must exist.") -// .total_staked_amount(), -// stake_amount_3 - unstake_amount -// ); -// } - -// #[test] -// fn contract_staking_info_series_unstake_with_inconsistent_data_fails() { -// let mut series = ContractStakingInfoSeries::default(); -// let era = 5; -// let period = 2; -// let period_info = PeriodInfo { -// number: period, -// period_type: PeriodType::Voting, -// ending_era: 31, -// }; - -// // 1st - Unstake from empty series -// assert!(series.unstake(1, period_info, era).is_err()); - -// // 2nd - Unstake with old period -// let amount = 37; -// assert!(series.stake(amount, period_info, era).is_ok()); - -// let old_period_info = { -// let mut temp = period_info.clone(); -// temp.number -= 1; -// temp -// }; -// assert!(series.unstake(1, old_period_info, era - 1).is_err()); - -// // 3rd - Unstake with 'too' old era -// assert!(series.unstake(1, period_info, era - 2).is_err()); -// assert!(series.unstake(1, period_info, era - 1).is_ok()); -// } - -// #[test] -// fn era_reward_span_push_and_get_works() { -// get_u32_type!(SpanLength, 8); -// let mut era_reward_span = EraRewardSpan::::new(); - -// // Sanity checks -// assert!(era_reward_span.is_empty()); -// assert!(era_reward_span.len().is_zero()); -// assert!(era_reward_span.first_era().is_zero()); -// assert!(era_reward_span.last_era().is_zero()); - -// // Insert some values and verify state change -// let era_1 = 5; -// let era_reward_1 = EraReward::new(23, 41); -// assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); -// assert_eq!(era_reward_span.len(), 1); -// assert_eq!(era_reward_span.first_era(), era_1); -// assert_eq!(era_reward_span.last_era(), era_1); - -// // Insert another value and verify state change -// let era_2 = era_1 + 1; -// let era_reward_2 = EraReward::new(37, 53); -// assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); -// assert_eq!(era_reward_span.len(), 2); -// assert_eq!(era_reward_span.first_era(), era_1); -// assert_eq!(era_reward_span.last_era(), era_2); - -// // Get the values and verify they are as expected -// assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); -// assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); -// } - -// #[test] -// fn era_reward_span_fails_when_expected() { -// // Capacity is only 2 to make testing easier -// get_u32_type!(SpanLength, 2); -// let mut era_reward_span = EraRewardSpan::::new(); - -// // Push first values to get started -// let era_1 = 5; -// let era_reward = EraReward::new(23, 41); -// assert!(era_reward_span.push(era_1, era_reward).is_ok()); - -// // Attempting to push incorrect era results in an error -// for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { -// assert_eq!( -// era_reward_span.push(*wrong_era, era_reward), -// Err(EraRewardSpanError::InvalidEra) -// ); -// } - -// // Pushing above capacity results in an error -// let era_2 = era_1 + 1; -// assert!(era_reward_span.push(era_2, era_reward).is_ok()); -// let era_3 = era_2 + 1; -// assert_eq!( -// era_reward_span.push(era_3, era_reward), -// Err(EraRewardSpanError::NoCapacity) -// ); -// } +#[test] +fn singular_staking_info_basics_are_ok() { + let period_number = 3; + let period_type = PeriodType::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, period_type); + + // Sanity checks + assert_eq!(staking_info.period_number(), period_number); + assert!(staking_info.is_loyal()); + assert!(staking_info.total_staked_amount().is_zero()); + assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); + + // Add some staked amount during `Voting` period + let vote_stake_amount_1 = 11; + staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert!(staking_info + .staked_amount(PeriodType::BuildAndEarn) + .is_zero()); + + // Add some staked amount during `BuildAndEarn` period + let bep_stake_amount_1 = 23; + staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + assert_eq!( + staking_info.total_staked_amount(), + vote_stake_amount_1 + bep_stake_amount_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::BuildAndEarn), + bep_stake_amount_1 + ); +} + +#[test] +fn singular_staking_info_unstake_during_voting_is_ok() { + let period_number = 3; + let period_type = PeriodType::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, period_type); + + // Prep actions + let vote_stake_amount_1 = 11; + staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + + // Unstake some amount during `Voting` period, loyalty should remain as expected. + let unstake_amount_1 = 5; + assert_eq!( + staking_info.unstake(unstake_amount_1, PeriodType::Voting), + (unstake_amount_1, Balance::zero()) + ); + assert_eq!( + staking_info.total_staked_amount(), + vote_stake_amount_1 - unstake_amount_1 + ); + assert!(staking_info.is_loyal()); + + // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. + let remaining_stake = staking_info.total_staked_amount(); + assert_eq!( + staking_info.unstake(remaining_stake + 1, PeriodType::Voting), + (remaining_stake, Balance::zero()) + ); + assert!(staking_info.total_staked_amount().is_zero()); + assert!(staking_info.is_loyal()); +} + +#[test] +fn singular_staking_info_unstake_during_bep_is_ok() { + let period_number = 3; + let period_type = PeriodType::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, period_type); + + // Prep actions + let vote_stake_amount_1 = 11; + staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + let bep_stake_amount_1 = 23; + staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + + // 1st scenario - Unstake some of the amount staked during B&E period + let unstake_1 = 5; + assert_eq!( + staking_info.unstake(5, PeriodType::BuildAndEarn), + (Balance::zero(), unstake_1) + ); + assert_eq!( + staking_info.total_staked_amount(), + vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::BuildAndEarn), + bep_stake_amount_1 - unstake_1 + ); + assert!(staking_info.is_loyal()); + + // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. + // The point is to take a chunk from the voting period stake too. + let current_total_stake = staking_info.total_staked_amount(); + let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); + let voting_stake_overflow = 2; + let unstake_2 = current_bep_stake + voting_stake_overflow; + + assert_eq!( + staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), + (voting_stake_overflow, current_bep_stake) + ); + assert_eq!( + staking_info.total_staked_amount(), + current_total_stake - unstake_2 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 - voting_stake_overflow + ); + assert!(staking_info + .staked_amount(PeriodType::BuildAndEarn) + .is_zero()); + assert!( + !staking_info.is_loyal(), + "Loyalty flag should have been removed due to non-zero voting period unstake" + ); +} + +#[test] +fn contract_stake_amount_info_series_get_works() { + let info_1 = StakeAmount::new(0, 0, 4, 2); + let info_2 = StakeAmount::new(11, 0, 7, 3); + let info_3 = StakeAmount::new(0, 13, 9, 3); + + let series = ContractStakeAmountSeries::new(vec![info_1, info_2, info_3]); + + // Sanity check + assert_eq!(series.len(), 3); + assert!(!series.is_empty()); + + // 1st scenario - get existing entries + assert_eq!(series.get(4, 2), Some(info_1)); + assert_eq!(series.get(7, 3), Some(info_2)); + assert_eq!(series.get(9, 3), Some(info_3)); + + // 2nd scenario - get non-existing entries for covered eras + { + let era_1 = 6; + let entry_1 = series.get(era_1, 2).expect("Has to be Some"); + assert!(entry_1.total().is_zero()); + assert_eq!(entry_1.era, era_1); + assert_eq!(entry_1.period, 2); + + let era_2 = 8; + let entry_1 = series.get(era_2, 3).expect("Has to be Some"); + assert_eq!(entry_1.total(), 11); + assert_eq!(entry_1.era, era_2); + assert_eq!(entry_1.period, 3); + } + + // 3rd scenario - get non-existing entries for covered eras but mismatching period + assert!(series.get(8, 2).is_none()); + + // 4th scenario - get non-existing entries for non-covered eras + assert!(series.get(3, 2).is_none()); +} + +#[test] +fn contract_stake_amount_info_series_stake_is_ok() { + let mut series = ContractStakeAmountSeries::default(); + + // Sanity check + assert!(series.is_empty()); + assert!(series.len().is_zero()); + + // 1st scenario - stake some amount and verify state change + let era_1 = 3; + let period_1 = 5; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); + let amount_1 = 31; + assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); + + assert_eq!(series.len(), 1); + assert!(!series.is_empty()); + + let entry_1_1 = series.get(era_1, period_1).unwrap(); + assert_eq!(entry_1_1.era, era_1); + assert_eq!(entry_1_1.total(), amount_1); + + // 2nd scenario - stake some more to the same era but different period type, and verify state change. + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); + assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); + assert_eq!( + series.len(), + 1, + "No new entry should be created since it's the same era." + ); + let entry_1_2 = series.get(era_1, period_1).unwrap(); + assert_eq!(entry_1_2.era, era_1); + assert_eq!(entry_1_2.total(), amount_1 * 2); + + // 3rd scenario - stake more to the next era, while still in the same period. + let era_2 = era_1 + 2; + let amount_2 = 37; + assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); + assert_eq!(series.len(), 2); + let entry_2_1 = series.get(era_1, period_1).unwrap(); + let entry_2_2 = series.get(era_2, period_1).unwrap(); + assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); + assert_eq!(entry_2_2.era, era_2); + assert_eq!(entry_2_2.period, period_1); + assert_eq!( + entry_2_2.total(), + entry_2_1.total() + amount_2, + "Since it's the same period, stake amount must carry over from the previous entry." + ); + + // 4th scenario - stake some more to the next era, but this time also bump the period. + let era_3 = era_2 + 3; + let period_2 = period_1 + 1; + let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); + let amount_3 = 41; + + assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); + assert_eq!(series.len(), 3); + let entry_3_1 = series.get(era_1, period_1).unwrap(); + let entry_3_2 = series.get(era_2, period_1).unwrap(); + let entry_3_3 = series.get(era_3, period_2).unwrap(); + assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); + assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); + assert_eq!(entry_3_3.era, era_3); + assert_eq!(entry_3_3.period, period_2); + assert_eq!( + entry_3_3.total(), + amount_3, + "No carry over from previous entry since period has changed." + ); + + // 5th scenario - stake to the next era, expect cleanup of oldest entry + let era_4 = era_3 + 1; + let amount_4 = 5; + assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); + assert_eq!(series.len(), 3); + let entry_4_1 = series.get(era_2, period_1).unwrap(); + let entry_4_2 = series.get(era_3, period_2).unwrap(); + let entry_4_3 = series.get(era_4, period_2).unwrap(); + assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); + assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); + assert_eq!(entry_4_3.era, era_4); + assert_eq!(entry_4_3.period, period_2); + assert_eq!(entry_4_3.total(), amount_3 + amount_4); +} + +#[test] +fn contract_stake_amount_info_series_stake_with_inconsistent_data_fails() { + let mut series = ContractStakeAmountSeries::default(); + + // Create an entry with some staked amount + let era = 5; + let period_info = PeriodInfo { + number: 7, + period_type: PeriodType::Voting, + ending_era: 31, + }; + let amount = 37; + assert!(series.stake(amount, period_info, era).is_ok()); + + // 1st scenario - attempt to stake using old era + assert!(series.stake(amount, period_info, era - 1).is_err()); + + // 2nd scenario - attempt to stake using old period + let period_info = PeriodInfo { + number: period_info.number - 1, + period_type: PeriodType::Voting, + ending_era: 31, + }; + assert!(series.stake(amount, period_info, era).is_err()); +} + +#[test] +fn contract_stake_amount_info_series_unstake_is_ok() { + let mut series = ContractStakeAmountSeries::default(); + + // Prep action - create a stake entry + let era_1 = 2; + let period = 3; + let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); + let stake_amount = 100; + assert!(series.stake(stake_amount, period_info, era_1).is_ok()); + + // 1st scenario - unstake in the same era + let amount_1 = 5; + assert!(series.unstake(amount_1, period_info, era_1).is_ok()); + assert_eq!(series.len(), 1); + assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); + assert_eq!( + series.staked_amount(period, PeriodType::Voting), + stake_amount - amount_1 + ); + + // 2nd scenario - unstake in the future era, creating a 'gap' in the series + // [(era: 2)] ---> [(era: 2), (era: 5)] + let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); + let era_2 = era_1 + 3; + let amount_2 = 7; + assert!(series.unstake(amount_2, period_info, era_2).is_ok()); + assert_eq!(series.len(), 2); + assert_eq!( + series.total_staked_amount(period), + stake_amount - amount_1 - amount_2 + ); + assert_eq!( + series.staked_amount(period, PeriodType::Voting), + stake_amount - amount_1 - amount_2 + ); + + // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones + // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] + let era_3 = era_2 - 1; + let amount_3 = 11; + assert!(series.unstake(amount_3, period_info, era_3).is_ok()); + assert_eq!(series.len(), 3); + assert_eq!( + series.total_staked_amount(period), + stake_amount - amount_1 - amount_2 - amount_3 + ); + assert_eq!( + series.staked_amount(period, PeriodType::Voting), + stake_amount - amount_1 - amount_2 - amount_3 + ); + + // Check concrete entries + assert_eq!( + series.get(era_1, period).unwrap().total(), + stake_amount - amount_1, + "Oldest entry must remain unchanged." + ); + assert_eq!( + series.get(era_2, period).unwrap().total(), + stake_amount - amount_1 - amount_2 - amount_3, + "Future era entry must be updated with all of the reductions." + ); + assert_eq!( + series.get(era_3, period).unwrap().total(), + stake_amount - amount_1 - amount_3, + "Second to last era entry must be updated with first & last reduction\ + because it derives its initial value from the oldest entry." + ); +} + +#[test] +fn contract_stake_amount_info_unstake_with_worst_case_scenario_for_capacity_overflow() { + let (era_1, era_2, era_3) = (4, 7, 9); + let (period_1, period_2) = (2, 3); + let (stake_amount_2, stake_amount_3) = (11, 13); + let info_1 = StakeAmount::new(11, 0, era_1, period_1); + let info_2 = StakeAmount::new(stake_amount_2, 0, era_2, period_2); + let info_3 = StakeAmount::new(0, stake_amount_3, era_3, period_2); + + // A gap between 2nd and 3rd era, and from that gap unstake will be done. + // This will force a new entry to be created, potentially overflowing the vector capacity. + let mut series = ContractStakeAmountSeries::new(vec![info_1, info_2, info_3]); + + // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity + let period_info = PeriodInfo { + number: period_2, + period_type: PeriodType::BuildAndEarn, + ending_era: 51, + }; + let unstake_amount = 3; + assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); + assert_eq!(series.len(), 3); + + assert_eq!( + series.get(era_1, period_1), + None, + "Oldest entry should have been prunned" + ); + assert_eq!( + series + .get(era_2, period_2) + .expect("Entry must exist.") + .total(), + stake_amount_2 + ); + assert_eq!( + series + .get(era_2 + 1, period_2) + .expect("Entry must exist.") + .total(), + stake_amount_2 - unstake_amount + ); + assert_eq!( + series + .get(era_3, period_2) + .expect("Entry must exist.") + .total(), + stake_amount_3 - unstake_amount + ); +} + +#[test] +fn contract_stake_amount_info_series_unstake_with_inconsistent_data_fails() { + let mut series = ContractStakeAmountSeries::default(); + let era = 5; + let period = 2; + let period_info = PeriodInfo { + number: period, + period_type: PeriodType::Voting, + ending_era: 31, + }; + + // 1st - Unstake from empty series + assert!(series.unstake(1, period_info, era).is_err()); + + // 2nd - Unstake with old period + let amount = 37; + assert!(series.stake(amount, period_info, era).is_ok()); + + let old_period_info = { + let mut temp = period_info.clone(); + temp.number -= 1; + temp + }; + assert!(series.unstake(1, old_period_info, era - 1).is_err()); + + // 3rd - Unstake with 'too' old era + assert!(series.unstake(1, period_info, era - 2).is_err()); + assert!(series.unstake(1, period_info, era - 1).is_ok()); +} + +#[test] +fn era_reward_span_push_and_get_works() { + get_u32_type!(SpanLength, 8); + let mut era_reward_span = EraRewardSpan::::new(); + + // Sanity checks + assert!(era_reward_span.is_empty()); + assert!(era_reward_span.len().is_zero()); + assert!(era_reward_span.first_era().is_zero()); + assert!(era_reward_span.last_era().is_zero()); + + // Insert some values and verify state change + let era_1 = 5; + let era_reward_1 = EraReward::new(23, 41); + assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); + assert_eq!(era_reward_span.len(), 1); + assert_eq!(era_reward_span.first_era(), era_1); + assert_eq!(era_reward_span.last_era(), era_1); + + // Insert another value and verify state change + let era_2 = era_1 + 1; + let era_reward_2 = EraReward::new(37, 53); + assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); + assert_eq!(era_reward_span.len(), 2); + assert_eq!(era_reward_span.first_era(), era_1); + assert_eq!(era_reward_span.last_era(), era_2); + + // Get the values and verify they are as expected + assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); + assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); +} + +#[test] +fn era_reward_span_fails_when_expected() { + // Capacity is only 2 to make testing easier + get_u32_type!(SpanLength, 2); + let mut era_reward_span = EraRewardSpan::::new(); + + // Push first values to get started + let era_1 = 5; + let era_reward = EraReward::new(23, 41); + assert!(era_reward_span.push(era_1, era_reward).is_ok()); + + // Attempting to push incorrect era results in an error + for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { + assert_eq!( + era_reward_span.push(*wrong_era, era_reward), + Err(EraRewardSpanError::InvalidEra) + ); + } + + // Pushing above capacity results in an error + let era_2 = era_1 + 1; + assert!(era_reward_span.push(era_2, era_reward).is_ok()); + let era_3 = era_2 + 1; + assert_eq!( + era_reward_span.push(era_3, era_reward), + Err(EraRewardSpanError::NoCapacity) + ); +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 1d15e0e548..8c418e60ba 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -26,16 +26,10 @@ use sp_runtime::{ use astar_primitives::Balance; -// use crate::pallet::Config; +use crate::pallet::Config; -// TODO: instead of using `pub` visiblity for fields, either use `pub(crate)` or add dedicated methods for accessing them. - -/// Convenience type for `AccountLedger` usage. -// pub type AccountLedgerFor = AccountLedger< -// BlockNumberFor, -// ::MaxUnlockingChunks, -// ::MaxStakingChunks, -// >; +// Convenience type for `AccountLedger` usage. +pub type AccountLedgerFor = AccountLedger, ::MaxUnlockingChunks>; /// Era number type pub type EraNumber = u32; @@ -462,13 +456,23 @@ where } // TODO: maybe the check can be nicer? - if self.staked.era > EraNumber::zero() { + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. if self.staked.era != era { return Err(AccountLedgerError::InvalidEra); } if self.staked.period != current_period_info.number { return Err(AccountLedgerError::InvalidPeriod); } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era + 1 { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } } if self.stakeable_amount(current_period_info.number) < amount { @@ -506,11 +510,24 @@ where return Ok(()); } - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); + // TODO: maybe the check can be nicer? (and not duplicated?) + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era + 1 { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } } // User must be precise with their unstake amount. @@ -520,8 +537,18 @@ where self.staked .subtract(amount, current_period_info.period_type); - if let Some(stake_amount) = self.staked_future.as_mut() { + // Convenience cleanup + if self.staked.is_empty() { + self.staked = Default::default(); + } + if let Some(mut stake_amount) = self.staked_future { stake_amount.subtract(amount, current_period_info.period_type); + + self.staked_future = if stake_amount.is_empty() { + None + } else { + Some(stake_amount) + }; } Ok(()) @@ -535,8 +562,9 @@ where &mut self, era: EraNumber, period_end: Option, - // TODO: add a better type later ) -> Result<(EraNumber, EraNumber, Balance), AccountLedgerError> { + // TODO: the check also needs to ensure that future entry is covered!!! + // TODO2: the return type won't work since we can have 2 distinct values - one from staked, one from staked_future if era <= self.staked.era || self.staked.total().is_zero() { return Err(AccountLedgerError::NothingToClaim); } @@ -817,9 +845,9 @@ const STAKING_SERIES_HISTORY: u32 = 3; /// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct StakeAmountSeries(BoundedVec>); -impl StakeAmountSeries { - /// Helper function to create a new instance of `StakeAmountSeries`. +pub struct ContractStakeAmountSeries(BoundedVec>); +impl ContractStakeAmountSeries { + /// Helper function to create a new instance of `ContractStakeAmountSeries`. #[cfg(test)] pub fn new(inner: Vec) -> Self { Self(BoundedVec::try_from(inner).expect("Test should ensure this is always valid")) @@ -872,16 +900,6 @@ impl StakeAmountSeries { } } - /// Last era for which a stake entry exists, `None` if no entries exist. - pub fn last_stake_era(&self) -> Option { - self.0.last().map(|info| info.era) - } - - /// Last period for which a stake entry exists, `None` if no entries exist. - pub fn last_stake_period(&self) -> Option { - self.0.last().map(|info| info.period) - } - /// Total staked amount on the contract, in the active period. pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { match self.0.last() { From e77327d37170cbd1d53bb064585642be57bc8b16 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 17 Oct 2023 11:55:32 +0200 Subject: [PATCH 09/86] Refactoring progress --- pallets/dapp-staking-v3/src/lib.rs | 207 +++++++------ pallets/dapp-staking-v3/src/test/mod.rs | 4 +- .../dapp-staking-v3/src/test/testing_utils.rs | 142 ++++----- pallets/dapp-staking-v3/src/test/tests.rs | 279 +++++------------- .../dapp-staking-v3/src/test/tests_types.rs | 51 ++-- pallets/dapp-staking-v3/src/types.rs | 136 ++++++++- 6 files changed, 388 insertions(+), 431 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8df02a4f90..3e3eef7acd 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -806,10 +806,11 @@ pub mod pallet { ); let protocol_state = ActiveProtocolState::::get(); - // Staker always stakes from the NEXT era - let stake_era = protocol_state.era.saturating_add(1); + let stake_era = protocol_state.era; ensure!( - !protocol_state.period_info.is_next_period(stake_era), + !protocol_state + .period_info + .is_next_period(stake_era.saturating_add(1)), Error::::PeriodEndsInNextEra ); @@ -820,7 +821,7 @@ pub mod pallet { ledger .add_stake_amount(amount, stake_era, protocol_state.period_info) .map_err(|err| match err { - AccountLedgerError::InvalidPeriod => { + AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewardsFromPastPeriods } AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, @@ -952,7 +953,9 @@ pub mod pallet { ledger .unstake_amount(amount, unstake_era, protocol_state.period_info) .map_err(|err| match err { - AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, + AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { + Error::::UnclaimedRewardsFromPastPeriods + } AccountLedgerError::UnstakeAmountLargerThanStake => { Error::::UnstakeAmountTooLarge } @@ -995,109 +998,97 @@ pub mod pallet { Ok(()) } - // /// TODO - // #[pallet::call_index(11)] - // #[pallet::weight(Weight::zero())] - // pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { - // Self::ensure_pallet_enabled()?; - // let account = ensure_signed(origin)?; - - // let protocol_state = ActiveProtocolState::::get(); - // let mut ledger = Ledger::::get(&account); - - // // TODO: how do we handle expired rewards? Add an additional call to clean them up? - // // Putting this logic inside existing calls will add even more complexity. - - // // Check if the rewards have expired - // let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; - // ensure!( - // staked_period - // >= protocol_state - // .period_number() - // .saturating_sub(T::RewardRetentionInPeriods::get()), - // Error::::StakerRewardsExpired - // ); - - // // Calculate the reward claim span - // let (first_chunk, last_chunk) = ledger - // .first_and_last_stake_chunks() - // .ok_or(Error::::InternalClaimStakerError)?; - // let era_rewards = - // EraRewards::::get(Self::era_reward_span_index(first_chunk.get_era())) - // .ok_or(Error::::NoClaimableRewards)?; - - // // The last era for which we can theoretically claim rewards. - // // And indicator if we know the period's ending era. - // let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { - // (protocol_state.era.saturating_sub(1), None) - // } else { - // PeriodEnd::::get(&staked_period) - // .map(|info| (info.final_era, Some(info.final_era))) - // .ok_or(Error::::InternalClaimStakerError)? - // }; - - // // The last era for which we can claim rewards for this account. - // // Limiting factors are: - // // 1. era reward span entries - // // 2. last era in period limit - // // 3. last era in which staker had non-zero stake - // let last_claim_era = if last_chunk.get_amount().is_zero() { - // era_rewards - // .last_era() - // .min(last_period_era) - // .min(last_chunk.get_era()) - // } else { - // era_rewards.last_era().min(last_period_era) - // }; - - // // Get chunks for reward claiming - // let chunks_for_claim = - // ledger - // .claim_up_to_era(last_claim_era, period_end) - // .map_err(|err| match err { - // AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, - // _ => Error::::InternalClaimStakerError, - // })?; - - // // Calculate rewards - // let mut rewards: Vec<_> = Vec::new(); - // let mut reward_sum = Balance::zero(); - // for era in first_chunk.get_era()..=last_claim_era { - // // TODO: this should be zipped, and values should be fetched only once - // let era_reward = era_rewards - // .get(era) - // .ok_or(Error::::InternalClaimStakerError)?; - - // let chunk = chunks_for_claim - // .get(era) - // .ok_or(Error::::InternalClaimStakerError)?; - - // // Optimization, and zero-division protection - // if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { - // continue; - // } - // let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) - // * era_reward.staker_reward_pool(); - - // rewards.push((era, staker_reward)); - // reward_sum.saturating_accrue(staker_reward); - // } - - // T::Currency::deposit_into_existing(&account, reward_sum) - // .map_err(|_| Error::::InternalClaimStakerError)?; - - // Self::update_ledger(&account, ledger); - - // rewards.into_iter().for_each(|(era, reward)| { - // Self::deposit_event(Event::::Reward { - // account: account.clone(), - // era, - // amount: reward, - // }); - // }); - - // Ok(()) - // } + /// TODO + #[pallet::call_index(11)] + #[pallet::weight(Weight::zero())] + pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let protocol_state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + // TODO: how do we handle expired rewards? Add an additional call to clean them up? + // Putting this logic inside existing calls will add even more complexity. + + // Check if the rewards have expired + let staked_period = ledger + .staked_period() + .ok_or(Error::::NoClaimableRewards)?; + ensure!( + staked_period + >= protocol_state + .period_number() + .saturating_sub(T::RewardRetentionInPeriods::get()), + Error::::StakerRewardsExpired + ); + + // Calculate the reward claim span + let earliest_staked_era = ledger + .earliest_staked_era() + .ok_or(Error::::InternalClaimStakerError)?; + let era_rewards = + EraRewards::::get(Self::era_reward_span_index(earliest_staked_era)) + .ok_or(Error::::NoClaimableRewards)?; + + // The last era for which we can theoretically claim rewards. + // And indicator if we know the period's ending era. + let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { + (protocol_state.era.saturating_sub(1), None) + } else { + PeriodEnd::::get(&staked_period) + .map(|info| (info.final_era, Some(info.final_era))) + .ok_or(Error::::InternalClaimStakerError)? + }; + + // The last era for which we can claim rewards for this account. + let last_claim_era = era_rewards.last_era().min(last_period_era); + + // Get chunks for reward claiming + let rewards_iter = + ledger + .claim_up_to_era(last_claim_era, period_end) + .map_err(|err| match err { + AccountLedgerError::NothingToClaim => Error::::NoClaimableRewards, + _ => Error::::InternalClaimStakerError, + })?; + + // Calculate rewards + let mut rewards: Vec<_> = Vec::new(); + let mut reward_sum = Balance::zero(); + for (era, amount) in rewards_iter { + // TODO: this should be zipped, and values should be fetched only once + let era_reward = era_rewards + .get(era) + .ok_or(Error::::InternalClaimStakerError)?; + + // Optimization, and zero-division protection + if amount.is_zero() || era_reward.staked().is_zero() { + continue; + } + let staker_reward = Perbill::from_rational(amount, era_reward.staked()) + * era_reward.staker_reward_pool(); + + rewards.push((era, staker_reward)); + reward_sum.saturating_accrue(staker_reward); + } + + // TODO: add negative test for this. + T::Currency::deposit_into_existing(&account, reward_sum) + .map_err(|_| Error::::InternalClaimStakerError)?; + + Self::update_ledger(&account, ledger); + + rewards.into_iter().for_each(|(era, reward)| { + Self::deposit_event(Event::::Reward { + account: account.clone(), + era, + amount: reward, + }); + }); + + Ok(()) + } } impl Pallet { diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs index d4f5974862..94a090243c 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -17,6 +17,6 @@ // along with Astar. If not, see . mod mock; -// mod testing_utils; -// mod tests; +mod testing_utils; +mod tests; mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index ccc171a46b..6657e02c2b 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -21,7 +21,7 @@ use crate::types::*; use crate::{ pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, - PeriodEndInfo, StakerInfo, + PeriodEndInfo, StakeAmount, StakerInfo, }; use frame_support::{assert_ok, traits::Get}; @@ -48,7 +48,7 @@ pub(crate) struct MemorySnapshot { SingularStakingInfo, >, contract_stake: - HashMap<::SmartContract, ContractStakingInfoSeries>, + HashMap<::SmartContract, ContractStakeAmountSeries>, era_rewards: HashMap< EraNumber, EraRewardSpan<::EraRewardSpanLength>, @@ -422,7 +422,7 @@ pub(crate) fn assert_stake( let pre_contract_stake = pre_snapshot .contract_stake .get(&smart_contract) - .map_or(ContractStakingInfoSeries::default(), |series| { + .map_or(ContractStakeAmountSeries::default(), |series| { series.clone() }); let pre_era_info = pre_snapshot.current_era_info; @@ -459,7 +459,11 @@ pub(crate) fn assert_stake( // 1. verify ledger // ===================== // ===================== - assert_eq!(post_ledger.staked_period, Some(stake_period)); + assert_eq!( + post_ledger.staked, pre_ledger.staked, + "Must remain exactly the same." + ); + assert_eq!(post_ledger.staked_future.unwrap().period, stake_period); assert_eq!( post_ledger.staked_amount(stake_period), pre_ledger.staked_amount(stake_period) + amount, @@ -470,22 +474,7 @@ pub(crate) fn assert_stake( pre_ledger.stakeable_amount(stake_period) - amount, "Stakeable amount must decrease by the 'amount'" ); - match pre_ledger.last_stake_era() { - Some(last_stake_era) if last_stake_era == stake_era => { - assert_eq!( - post_ledger.staked.0.len(), - pre_ledger.staked.0.len(), - "Existing entry must be modified." - ); - } - _ => { - assert_eq!( - post_ledger.staked.0.len(), - pre_ledger.staked.0.len() + 1, - "Additional entry must be added." - ); - } - } + // TODO: maybe expand checks here? // 2. verify staker info // ===================== @@ -534,39 +523,20 @@ pub(crate) fn assert_stake( // 3. verify contract stake // ========================= // ========================= - // TODO: since default value is all zeros, maybe we can just skip the branching code and do it once? - match pre_contract_stake.last_stake_period() { - Some(last_stake_period) if last_stake_period == stake_period => { - assert_eq!( - post_contract_stake.total_staked_amount(stake_period), - pre_contract_stake.total_staked_amount(stake_period) + amount, - "Staked amount must increase by the 'amount'" - ); - assert_eq!( - post_contract_stake.staked_amount(stake_period, stake_period_type), - pre_contract_stake.staked_amount(stake_period, stake_period_type) + amount, - "Staked amount must increase by the 'amount'" - ); - } - _ => { - assert_eq!(post_contract_stake.len(), 1); - assert_eq!( - post_contract_stake.total_staked_amount(stake_period), - amount, - "Total staked amount must be equal to exactly the 'amount'" - ); - assert_eq!( - post_contract_stake.staked_amount(stake_period, stake_period_type), - amount, - "Staked amount must be equal to exactly the 'amount'" - ); - } - } + assert_eq!( + post_contract_stake.total_staked_amount(stake_period), + pre_contract_stake.total_staked_amount(stake_period) + amount, + "Staked amount must increase by the 'amount'" + ); + assert_eq!( + post_contract_stake.staked_amount(stake_period, stake_period_type), + pre_contract_stake.staked_amount(stake_period, stake_period_type) + amount, + "Staked amount must increase by the 'amount'" + ); + assert_eq!(post_contract_stake.last_stake_period(), Some(stake_period)); assert_eq!(post_contract_stake.last_stake_era(), Some(stake_era)); - // TODO: expand this check to compare inner slices as well! - // 4. verify era info // ========================= // ========================= @@ -643,7 +613,6 @@ pub(crate) fn assert_unstake( // 1. verify ledger // ===================== // ===================== - assert_eq!(post_ledger.staked_period, Some(unstake_period)); assert_eq!( post_ledger.staked_amount(unstake_period), pre_ledger.staked_amount(unstake_period) - amount, @@ -654,7 +623,7 @@ pub(crate) fn assert_unstake( pre_ledger.stakeable_amount(unstake_period) + amount, "Stakeable amount must increase by the 'amount'" ); - // TODO: maybe extend check with concrete value checks? E.g. if we modify past entry, we should check past & current entries are properly adjusted. + // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -709,7 +678,6 @@ pub(crate) fn assert_unstake( .saturating_sub(amount), "Staked amount must decreased by the 'amount'" ); - // TODO: extend with concrete value checks later // 4. verify era info // ========================= @@ -760,11 +728,8 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { // Get the first eligible era for claiming rewards let first_claim_era = pre_ledger - .staked - .0 - .first() - .expect("Entry must exist, otherwise 'claim' is invalid.") - .get_era(); + .earliest_staked_era() + .expect("Entry must exist, otherwise 'claim' is invalid."); // Get the apprropriate era rewards span for the 'first era' let era_span_length: EraNumber = @@ -776,19 +741,24 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { .expect("Entry must exist, otherwise 'claim' is invalid."); // Calculate the final era for claiming rewards. Also determine if this will fully claim all staked period rewards. - let is_current_period_stake = match pre_ledger.staked_period { - Some(staked_period) - if staked_period == pre_snapshot.active_protocol_state.period_number() => - { - true - } - _ => false, + let claim_period_end = if pre_ledger.staked_period().unwrap() + == pre_snapshot.active_protocol_state.period_number() + { + None + } else { + Some( + pre_snapshot + .period_end + .get(&pre_ledger.staked_period().unwrap()) + .expect("Entry must exist, since it's the current period.") + .final_era, + ) }; - let (last_claim_era, is_full_claim) = if is_current_period_stake { + let (last_claim_era, is_full_claim) = if claim_period_end.is_none() { (pre_snapshot.active_protocol_state.era - 1, false) } else { - let claim_period = pre_ledger.staked_period.unwrap(); + let claim_period = pre_ledger.staked_period().unwrap(); let period_end = pre_snapshot .period_end .get(&claim_period) @@ -806,16 +776,16 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { // Calculate the expected rewards let mut rewards = Vec::new(); - for era in first_claim_era..=last_claim_era { + for (era, amount) in pre_ledger + .clone() + .claim_up_to_era(last_claim_era, claim_period_end) + .unwrap() + { let era_reward_info = era_rewards_span .get(era) .expect("Entry must exist, otherwise 'claim' is invalid."); - let stake_chunk = pre_ledger - .staked - .get(era) - .expect("Entry must exist, otherwise 'claim' is invalid."); - let reward = Perbill::from_rational(stake_chunk.amount, era_reward_info.staked()) + let reward = Perbill::from_rational(amount, era_reward_info.staked()) * era_reward_info.staker_reward_pool(); if reward.is_zero() { continue; @@ -868,15 +838,11 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let post_ledger = post_snapshot.ledger.get(&account).unwrap(); if is_full_claim { - assert!(post_ledger.staked.0.is_empty()); - assert!(post_ledger.staked_period.is_none()); + assert!(post_ledger.staked.is_empty()); + assert!(post_ledger.staked_future.is_none()); } else { - let stake_chunk = post_ledger.staked.0.first().expect("Entry must exist"); - assert_eq!(stake_chunk.era, last_claim_era + 1); - assert_eq!( - stake_chunk.amount, - pre_ledger.staked.get(last_claim_era).unwrap().amount - ); + assert_eq!(post_ledger.staked.era, last_claim_era + 1); + // TODO: expand check? } } @@ -888,25 +854,21 @@ pub(crate) fn claimable_reward_range(account: AccountId) -> Option<(EraNumber, E let ledger = Ledger::::get(&account); let protocol_state = ActiveProtocolState::::get(); - let (first_chunk, last_chunk) = if let Some(chunks) = ledger.first_and_last_stake_chunks() { - chunks + let earliest_stake_era = if let Some(era) = ledger.earliest_staked_era() { + era } else { return None; }; - // Full unstake happened, no rewards past this. - let last_claim_era = if last_chunk.get_amount().is_zero() { - last_chunk.get_era() - 1 - } else if ledger.staked_period == Some(protocol_state.period_number()) { - // Staked in the ongoing period, best we can do is claim up to last era + let last_claim_era = if ledger.staked_period() == Some(protocol_state.period_number()) { protocol_state.era - 1 } else { // Period finished, we can claim up to its final era - let period_end = PeriodEnd::::get(ledger.staked_period.unwrap()).unwrap(); + let period_end = PeriodEnd::::get(ledger.staked_period().unwrap()).unwrap(); period_end.final_era }; - Some((first_chunk.get_era(), last_claim_era)) + Some((earliest_stake_era, last_claim_era)) } /// Number of times it's required to call `claim_staker_rewards` to claim all pending rewards. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 56fe48118f..30d34d5543 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -756,27 +756,10 @@ fn stake_basic_example_is_ok() { let lock_amount = 300; assert_lock(account, lock_amount); - // 1st scenario - stake some amount, and then some more + // Stake some amount, and then some more let (stake_amount_1, stake_amount_2) = (31, 29); assert_stake(account, &smart_contract, stake_amount_1); assert_stake(account, &smart_contract, stake_amount_2); - - // 2nd scenario - stake in the next era - advance_to_next_era(); - let stake_amount_3 = 23; - assert_stake(account, &smart_contract, stake_amount_3); - - // 3rd scenario - advance era again but create a gap, and then stake - advance_to_era(ActiveProtocolState::::get().era + 2); - let stake_amount_4 = 19; - assert_stake(account, &smart_contract, stake_amount_4); - - // 4th scenario - advance period, and stake - // advance_to_next_era(); - // advance_to_next_period(); - // let stake_amount_5 = 17; - // assert_stake(account, &smart_contract, stake_amount_5); - // TODO: this can only be tested after reward claiming has been implemented!!! }) } @@ -892,34 +875,6 @@ fn stake_fails_if_not_enough_stakeable_funds_available() { }) } -#[test] -fn stake_fails_due_to_too_many_chunks() { - ExtBuilder::build().execute_with(|| { - // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); - let account = 3; - assert_register(1, &smart_contract); - let lock_amount = 500; - assert_lock(account, lock_amount); - - // Keep on staking & creating chunks until capacity is reached - for _ in 0..(::MaxStakingChunks::get()) { - advance_to_next_era(); - assert_stake(account, &smart_contract, 10); - } - - // Ensure we can still stake in the current era since an entry exists - assert_stake(account, &smart_contract, 10); - - // Staking in the next era results in error due to too many chunks - advance_to_next_era(); - assert_noop!( - DappStaking::stake(RuntimeOrigin::signed(account), smart_contract.clone(), 10), - Error::::TooManyStakeChunks - ); - }) -} - #[test] fn stake_fails_due_to_too_small_staking_amount() { ExtBuilder::build().execute_with(|| { @@ -960,6 +915,8 @@ fn stake_fails_due_to_too_small_staking_amount() { }) } +// TODO: add tests to cover staking & unstaking with unclaimed rewards! + #[test] fn unstake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { @@ -976,33 +933,11 @@ fn unstake_basic_example_is_ok() { let stake_amount_1 = 83; assert_stake(account, &smart_contract, stake_amount_1); - // 1st scenario - unstake some amount, in the current era. + // Unstake some amount, in the current era. let unstake_amount_1 = 3; assert_unstake(account, &smart_contract, unstake_amount_1); - // 2nd scenario - advance to next era/period type, and unstake some more - let unstake_amount_2 = 7; - let unstake_amount_3 = 11; - advance_to_next_era(); - assert_eq!( - ActiveProtocolState::::get().period_type(), - PeriodType::BuildAndEarn, - "Sanity check, period type change must happe." - ); - assert_unstake(account, &smart_contract, unstake_amount_2); - assert_unstake(account, &smart_contract, unstake_amount_3); - - // 3rd scenario - advance few eras to create a gap, and unstake some more - advance_to_era(ActiveProtocolState::::get().era + 3); - assert_unstake(account, &smart_contract, unstake_amount_3); - assert_unstake(account, &smart_contract, unstake_amount_2); - - // 4th scenario - perform a full unstake - advance_to_next_era(); - let full_unstake_amount = StakerInfo::::get(&account, &smart_contract) - .unwrap() - .total_staked_amount(); - assert_unstake(account, &smart_contract, full_unstake_amount); + // TODO: scenario where we unstake AFTER advancing an era and claiming rewards }) } @@ -1027,33 +962,6 @@ fn unstake_with_leftover_amount_below_minimum_works() { }) } -#[test] -fn unstake_with_entry_overflow_attempt_works() { - ExtBuilder::build().execute_with(|| { - // Register smart contract & lock some amount - let dev_account = 1; - let smart_contract = MockSmartContract::default(); - assert_register(dev_account, &smart_contract); - - let account = 2; - let amount = 300; - assert_lock(account, amount); - - assert_stake(account, &smart_contract, amount); - - // Advance one era, unstake some amount. The goal is to make a new entry. - advance_to_next_era(); - assert_unstake(account, &smart_contract, 11); - - // Advance 2 eras, stake some amount. This should create a new entry for the next era. - advance_to_era(ActiveProtocolState::::get().era + 2); - assert_stake(account, &smart_contract, 3); - - // Unstake some amount, which should result in the creation of the 4th entry, but the oldest one should be prunned. - assert_unstake(account, &smart_contract, 1); - }) -} - #[test] fn unstake_with_zero_amount_fails() { ExtBuilder::build().execute_with(|| { @@ -1207,106 +1115,77 @@ fn unstake_from_past_period_fails() { }) } -#[test] -fn unstake_fails_due_to_too_many_chunks() { - ExtBuilder::build().execute_with(|| { - // Register smart contract,lock & stake some amount - let smart_contract = MockSmartContract::default(); - let account = 2; - assert_register(1, &smart_contract); - let lock_amount = 1000; - assert_lock(account, lock_amount); - assert_stake(account, &smart_contract, lock_amount); - - // Keep on unstaking & creating chunks until capacity is reached - for _ in 0..(::MaxStakingChunks::get()) { - advance_to_next_era(); - assert_unstake(account, &smart_contract, 11); - } - - // Ensure we can still unstake in the current era since an entry exists - assert_unstake(account, &smart_contract, 10); - - // Staking in the next era results in error due to too many chunks - advance_to_next_era(); - assert_noop!( - DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract.clone(), 10), - Error::::TooManyStakeChunks - ); - }) -} - -#[test] -fn claim_staker_rewards_basic_example_is_ok() { - ExtBuilder::build().execute_with(|| { - // Register smart contract, lock&stake some amount - let dev_account = 1; - let smart_contract = MockSmartContract::default(); - assert_register(dev_account, &smart_contract); - - let account = 2; - let lock_amount = 300; - assert_lock(account, lock_amount); - let stake_amount = 93; - assert_stake(account, &smart_contract, stake_amount); - - // Advance into Build&Earn period, and allow one era to pass. Claim reward for 1 era. - advance_to_era(ActiveProtocolState::::get().era + 2); - assert_claim_staker_rewards(account); - - // Advance a few more eras, and claim multiple rewards this time. - advance_to_era(ActiveProtocolState::::get().era + 3); - assert_eq!( - ActiveProtocolState::::get().period_number(), - 1, - "Sanity check, we must still be in the 1st period." - ); - assert_claim_staker_rewards(account); - - // Advance into the next period, make sure we can still claim old rewards. - advance_to_next_period(); - for _ in 0..required_number_of_reward_claims(account) { - assert_claim_staker_rewards(account); - } - }) -} - -#[test] -fn claim_staker_rewards_no_claimable_rewards_fails() { - ExtBuilder::build().execute_with(|| { - // Register smart contract, lock&stake some amount - let dev_account = 1; - let smart_contract = MockSmartContract::default(); - assert_register(dev_account, &smart_contract); - - let account = 2; - let lock_amount = 300; - assert_lock(account, lock_amount); - - // 1st scenario - try to claim with no stake at all. - assert_noop!( - DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), - Error::::NoClaimableRewards, - ); - - // 2nd scenario - stake some amount, and try to claim in the same era. - // It's important this is the 1st era, when no `EraRewards` entry exists. - assert_eq!(ActiveProtocolState::::get().era, 1, "Sanity check"); - assert!(EraRewards::::iter().next().is_none(), "Sanity check"); - let stake_amount = 93; - assert_stake(account, &smart_contract, stake_amount); - assert_noop!( - DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), - Error::::NoClaimableRewards, - ); - - // 3rd scenario - move over to the next era, but we still expect failure because - // stake is valid from era 2 (current era), and we're trying to claim rewards for era 1. - advance_to_next_era(); - assert!(EraRewards::::iter().next().is_some(), "Sanity check"); - assert_noop!( - DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), - Error::::NoClaimableRewards, - ); - }) -} +// #[test] +// fn claim_staker_rewards_basic_example_is_ok() { +// ExtBuilder::build().execute_with(|| { +// // Register smart contract, lock&stake some amount +// let dev_account = 1; +// let smart_contract = MockSmartContract::default(); +// assert_register(dev_account, &smart_contract); + +// let account = 2; +// let lock_amount = 300; +// assert_lock(account, lock_amount); +// let stake_amount = 93; +// assert_stake(account, &smart_contract, stake_amount); + +// // Advance into Build&Earn period, and allow one era to pass. Claim reward for 1 era. +// advance_to_era(ActiveProtocolState::::get().era + 2); +// assert_claim_staker_rewards(account); + +// // Advance a few more eras, and claim multiple rewards this time. +// advance_to_era(ActiveProtocolState::::get().era + 3); +// assert_eq!( +// ActiveProtocolState::::get().period_number(), +// 1, +// "Sanity check, we must still be in the 1st period." +// ); +// assert_claim_staker_rewards(account); + +// // Advance into the next period, make sure we can still claim old rewards. +// advance_to_next_period(); +// for _ in 0..required_number_of_reward_claims(account) { +// assert_claim_staker_rewards(account); +// } +// }) +// } + +// #[test] +// fn claim_staker_rewards_no_claimable_rewards_fails() { +// ExtBuilder::build().execute_with(|| { +// // Register smart contract, lock&stake some amount +// let dev_account = 1; +// let smart_contract = MockSmartContract::default(); +// assert_register(dev_account, &smart_contract); + +// let account = 2; +// let lock_amount = 300; +// assert_lock(account, lock_amount); + +// // 1st scenario - try to claim with no stake at all. +// assert_noop!( +// DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), +// Error::::NoClaimableRewards, +// ); + +// // 2nd scenario - stake some amount, and try to claim in the same era. +// // It's important this is the 1st era, when no `EraRewards` entry exists. +// assert_eq!(ActiveProtocolState::::get().era, 1, "Sanity check"); +// assert!(EraRewards::::iter().next().is_none(), "Sanity check"); +// let stake_amount = 93; +// assert_stake(account, &smart_contract, stake_amount); +// assert_noop!( +// DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), +// Error::::NoClaimableRewards, +// ); + +// // 3rd scenario - move over to the next era, but we still expect failure because +// // stake is valid from era 2 (current era), and we're trying to claim rewards for era 1. +// advance_to_next_era(); +// assert!(EraRewards::::iter().next().is_some(), "Sanity check"); +// assert_noop!( +// DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), +// Error::::NoClaimableRewards, +// ); +// }) +// } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 5ad99cb1a2..5a7399e4ea 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1188,6 +1188,7 @@ fn contract_stake_amount_info_series_stake_is_ok() { // 1st scenario - stake some amount and verify state change let era_1 = 3; + let stake_era_1 = era_1 + 1; let period_1 = 5; let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); let amount_1 = 31; @@ -1196,8 +1197,15 @@ fn contract_stake_amount_info_series_stake_is_ok() { assert_eq!(series.len(), 1); assert!(!series.is_empty()); - let entry_1_1 = series.get(era_1, period_1).unwrap(); - assert_eq!(entry_1_1.era, era_1); + assert!( + series.get(era_1, period_1).is_none(), + "Entry for current era must not exist." + ); + let entry_1_1 = series.get(stake_era_1, period_1).unwrap(); + assert_eq!( + entry_1_1.era, stake_era_1, + "Stake is only valid from next era." + ); assert_eq!(entry_1_1.total(), amount_1); // 2nd scenario - stake some more to the same era but different period type, and verify state change. @@ -1208,19 +1216,20 @@ fn contract_stake_amount_info_series_stake_is_ok() { 1, "No new entry should be created since it's the same era." ); - let entry_1_2 = series.get(era_1, period_1).unwrap(); - assert_eq!(entry_1_2.era, era_1); + let entry_1_2 = series.get(stake_era_1, period_1).unwrap(); + assert_eq!(entry_1_2.era, stake_era_1); assert_eq!(entry_1_2.total(), amount_1 * 2); // 3rd scenario - stake more to the next era, while still in the same period. let era_2 = era_1 + 2; + let stake_era_2 = era_2 + 1; let amount_2 = 37; assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); assert_eq!(series.len(), 2); - let entry_2_1 = series.get(era_1, period_1).unwrap(); - let entry_2_2 = series.get(era_2, period_1).unwrap(); + let entry_2_1 = series.get(stake_era_1, period_1).unwrap(); + let entry_2_2 = series.get(stake_era_2, period_1).unwrap(); assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); - assert_eq!(entry_2_2.era, era_2); + assert_eq!(entry_2_2.era, stake_era_2); assert_eq!(entry_2_2.period, period_1); assert_eq!( entry_2_2.total(), @@ -1230,18 +1239,19 @@ fn contract_stake_amount_info_series_stake_is_ok() { // 4th scenario - stake some more to the next era, but this time also bump the period. let era_3 = era_2 + 3; + let stake_era_3 = era_3 + 1; let period_2 = period_1 + 1; let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); let amount_3 = 41; assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); assert_eq!(series.len(), 3); - let entry_3_1 = series.get(era_1, period_1).unwrap(); - let entry_3_2 = series.get(era_2, period_1).unwrap(); - let entry_3_3 = series.get(era_3, period_2).unwrap(); + let entry_3_1 = series.get(stake_era_1, period_1).unwrap(); + let entry_3_2 = series.get(stake_era_2, period_1).unwrap(); + let entry_3_3 = series.get(stake_era_3, period_2).unwrap(); assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); - assert_eq!(entry_3_3.era, era_3); + assert_eq!(entry_3_3.era, stake_era_3); assert_eq!(entry_3_3.period, period_2); assert_eq!( entry_3_3.total(), @@ -1251,15 +1261,16 @@ fn contract_stake_amount_info_series_stake_is_ok() { // 5th scenario - stake to the next era, expect cleanup of oldest entry let era_4 = era_3 + 1; + let stake_era_4 = era_4 + 1; let amount_4 = 5; assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); assert_eq!(series.len(), 3); - let entry_4_1 = series.get(era_2, period_1).unwrap(); - let entry_4_2 = series.get(era_3, period_2).unwrap(); - let entry_4_3 = series.get(era_4, period_2).unwrap(); + let entry_4_1 = series.get(stake_era_2, period_1).unwrap(); + let entry_4_2 = series.get(stake_era_3, period_2).unwrap(); + let entry_4_3 = series.get(stake_era_4, period_2).unwrap(); assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); - assert_eq!(entry_4_3.era, era_4); + assert_eq!(entry_4_3.era, stake_era_4); assert_eq!(entry_4_3.period, period_2); assert_eq!(entry_4_3.total(), amount_3 + amount_4); } @@ -1296,6 +1307,7 @@ fn contract_stake_amount_info_series_unstake_is_ok() { // Prep action - create a stake entry let era_1 = 2; + let stake_era_1 = era_1 + 1; let period = 3; let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); let stake_amount = 100; @@ -1344,7 +1356,7 @@ fn contract_stake_amount_info_series_unstake_is_ok() { // Check concrete entries assert_eq!( - series.get(era_1, period).unwrap().total(), + series.get(stake_era_1, period).unwrap().total(), stake_amount - amount_1, "Oldest entry must remain unchanged." ); @@ -1416,6 +1428,7 @@ fn contract_stake_amount_info_unstake_with_worst_case_scenario_for_capacity_over fn contract_stake_amount_info_series_unstake_with_inconsistent_data_fails() { let mut series = ContractStakeAmountSeries::default(); let era = 5; + let stake_era = era + 1; let period = 2; let period_info = PeriodInfo { number: period, @@ -1435,11 +1448,11 @@ fn contract_stake_amount_info_series_unstake_with_inconsistent_data_fails() { temp.number -= 1; temp }; - assert!(series.unstake(1, old_period_info, era - 1).is_err()); + assert!(series.unstake(1, old_period_info, stake_era).is_err()); // 3rd - Unstake with 'too' old era - assert!(series.unstake(1, period_info, era - 2).is_err()); - assert!(series.unstake(1, period_info, era - 1).is_ok()); + assert!(series.unstake(1, period_info, stake_era - 2).is_err()); + assert!(series.unstake(1, period_info, stake_era - 1).is_ok()); } #[test] diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 8c418e60ba..907af2eb30 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -554,6 +554,24 @@ where Ok(()) } + // TODO + pub fn staked_period(&self) -> Option { + if self.staked.is_empty() { + self.staked_future.map(|stake_amount| stake_amount.period) + } else { + Some(self.staked.period) + } + } + + // TODO + pub fn earliest_staked_era(&self) -> Option { + if self.staked.is_empty() { + self.staked_future.map(|stake_amount| stake_amount.era) + } else { + Some(self.staked.era) + } + } + /// Claim up stake chunks up to the specified `era`. /// Returns the vector describing claimable chunks. /// @@ -562,22 +580,44 @@ where &mut self, era: EraNumber, period_end: Option, - ) -> Result<(EraNumber, EraNumber, Balance), AccountLedgerError> { - // TODO: the check also needs to ensure that future entry is covered!!! - // TODO2: the return type won't work since we can have 2 distinct values - one from staked, one from staked_future - if era <= self.staked.era || self.staked.total().is_zero() { + ) -> Result { + // Main entry exists, but era isn't 'in history' + if !self.staked.is_empty() && era <= self.staked.era { return Err(AccountLedgerError::NothingToClaim); + } else if let Some(stake_amount) = self.staked_future { + // Future entry exists, but era isn't 'in history' + if era <= stake_amount.era { + return Err(AccountLedgerError::NothingToClaim); + } } - let result = (self.staked.era, era, self.staked.total()); + // There are multiple options: + // 1. We only have future entry, no current entry + // 2. We have both current and future entry + // 3. We only have current entry, no future entry + let (span, maybe_other) = if let Some(stake_amount) = self.staked_future { + if self.staked.is_empty() { + ((stake_amount.era, era, stake_amount.total()), None) + } else { + ( + (stake_amount.era, era, stake_amount.total()), + Some((self.staked.era, self.staked.total())), + ) + } + } else { + ((self.staked.era, era, self.staked.total()), None) + }; + + let result = RewardsIter::new(span, maybe_other); // Update latest 'staked' era self.staked.era = era; - // Make sure to clean + // Make sure to clean up the entries match period_end { Some(ending_era) if era >= ending_era => { self.staker_rewards_claimed = true; + // TODO: probably not the right thing to do, need to check if pending bonus rewards need to be claimed as well self.staked = Default::default(); self.staked_future = None; } @@ -588,6 +628,56 @@ where } } +// TODO: docs & test +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RewardsIter { + maybe_other: Option<(EraNumber, Balance)>, + start_era: EraNumber, + end_era: EraNumber, + amount: Balance, +} + +impl RewardsIter { + pub fn new( + span: (EraNumber, EraNumber, Balance), + maybe_other: Option<(EraNumber, Balance)>, + ) -> Self { + if let Some((era, _amount)) = maybe_other { + debug_assert!( + span.0 == era + 1, + "The 'other', if it exists, must cover era preceding the span." + ); + } + + Self { + maybe_other, + start_era: span.0, + end_era: span.1, + amount: span.2, + } + } +} + +impl Iterator for RewardsIter { + type Item = (EraNumber, Balance); + + fn next(&mut self) -> Option { + // Fist cover the scenario where we have a unique first value + if let Some((era, amount)) = self.maybe_other.take() { + return Some((era, amount)); + } + + // Afterwards, just keep returning the same amount for different eras + if self.start_era <= self.end_era { + let value = (self.start_era, self.amount); + self.start_era.saturating_accrue(1); + return Some(value); + } else { + None + } + } +} + // TODO #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct StakeAmount { @@ -869,6 +959,16 @@ impl ContractStakeAmountSeries { self.0.is_empty() } + // TODO + pub fn last_stake_period(&self) -> Option { + self.0.last().map(|x| x.period) + } + + // TODO + pub fn last_stake_era(&self) -> Option { + self.0.last().map(|x| x.era) + } + /// Returns the `StakeAmount` type for the specified era & period, if it exists. pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { let idx = self @@ -923,18 +1023,20 @@ impl ContractStakeAmountSeries { &mut self, amount: Balance, period_info: PeriodInfo, - era: EraNumber, + current_era: EraNumber, ) -> Result<(), ()> { + let stake_era = current_era.saturating_add(1); + // Defensive check to ensure we don't end up in a corrupted state. Should never happen. if let Some(stake_amount) = self.0.last() { - if stake_amount.era > era || stake_amount.period > period_info.number { + if stake_amount.era > stake_era || stake_amount.period > period_info.number { return Err(()); } } // Get the most relevant `StakeAmount` instance let mut stake_amount = if let Some(stake_amount) = self.0.last() { - if stake_amount.era == era { + if stake_amount.era == stake_era { // Era matches, so we just update the last element. let stake_amount = *stake_amount; let _ = self.0.pop(); @@ -942,15 +1044,25 @@ impl ContractStakeAmountSeries { } else if stake_amount.period == period_info.number { // Periods match so we should 'copy' the last element to get correct staking amount let mut temp = *stake_amount; - temp.era = era; + temp.era = stake_era; temp } else { // It's a new period, so we need a completely new instance - StakeAmount::new(Balance::zero(), Balance::zero(), era, period_info.number) + StakeAmount::new( + Balance::zero(), + Balance::zero(), + stake_era, + period_info.number, + ) } } else { // It's a new period, so we need a completely new instance - StakeAmount::new(Balance::zero(), Balance::zero(), era, period_info.number) + StakeAmount::new( + Balance::zero(), + Balance::zero(), + stake_era, + period_info.number, + ) }; // Update the stake amount From 025f0b7c3a6d2751ab9a0266a0b2f94952def873 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 17 Oct 2023 14:09:08 +0200 Subject: [PATCH 10/86] Refactoring finished --- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- .../dapp-staking-v3/src/test/testing_utils.rs | 38 ++- pallets/dapp-staking-v3/src/test/tests.rs | 244 ++++++++++-------- .../dapp-staking-v3/src/test/tests_types.rs | 1 - pallets/dapp-staking-v3/src/types.rs | 11 +- 5 files changed, 161 insertions(+), 135 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index ab88c11773..35196ce042 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -229,7 +229,7 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn _advance_to_next_period_type() { +pub(crate) fn advance_to_next_period_type() { let period_type = ActiveProtocolState::::get().period_type(); while ActiveProtocolState::::get().period_type() == period_type { run_for_blocks(1); diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 6657e02c2b..1c328f852b 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -19,9 +19,8 @@ use crate::test::mock::*; use crate::types::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, ContractStake, - CurrentEraInfo, DAppId, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, - PeriodEndInfo, StakeAmount, StakerInfo, + pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, + EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, PeriodEndInfo, StakerInfo, }; use frame_support::{assert_ok, traits::Get}; @@ -36,23 +35,19 @@ pub(crate) struct MemorySnapshot { next_dapp_id: DAppId, current_era_info: EraInfo, integrated_dapps: HashMap< - ::SmartContract, + ::SmartContract, DAppInfo<::AccountId>, >, ledger: HashMap<::AccountId, AccountLedgerFor>, staker_info: HashMap< ( ::AccountId, - ::SmartContract, + ::SmartContract, ), SingularStakingInfo, >, - contract_stake: - HashMap<::SmartContract, ContractStakeAmountSeries>, - era_rewards: HashMap< - EraNumber, - EraRewardSpan<::EraRewardSpanLength>, - >, + contract_stake: HashMap<::SmartContract, ContractStakeAmountSeries>, + era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, } @@ -250,7 +245,7 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { // When unlocking would take account below the minimum lock threshold, unlock everything let locked_amount = pre_ledger.active_locked_amount(); - let min_locked_amount = ::MinimumLockedAmount::get(); + let min_locked_amount = ::MinimumLockedAmount::get(); if locked_amount.saturating_sub(possible_unlock_amount) < min_locked_amount { locked_amount } else { @@ -506,7 +501,7 @@ pub(crate) fn assert_stake( amount, "Total staked amount must be equal to exactly the 'amount'" ); - assert!(amount >= ::MinimumStakeAmount::get()); + assert!(amount >= ::MinimumStakeAmount::get()); assert_eq!( post_staker_info.staked_amount(stake_period_type), amount, @@ -577,8 +572,7 @@ pub(crate) fn assert_unstake( let unstake_period = pre_snapshot.active_protocol_state.period_number(); let unstake_period_type = pre_snapshot.active_protocol_state.period_type(); - let minimum_stake_amount: Balance = - ::MinimumStakeAmount::get(); + let minimum_stake_amount: Balance = ::MinimumStakeAmount::get(); let is_full_unstake = pre_staker_info.total_staked_amount().saturating_sub(amount) < minimum_stake_amount; @@ -723,8 +717,8 @@ pub(crate) fn assert_unstake( pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let pre_snapshot = MemorySnapshot::new(); let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); - let pre_total_issuance = ::Currency::total_issuance(); - let pre_free_balance = ::Currency::free_balance(&account); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(&account); // Get the first eligible era for claiming rewards let first_claim_era = pre_ledger @@ -732,8 +726,7 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { .expect("Entry must exist, otherwise 'claim' is invalid."); // Get the apprropriate era rewards span for the 'first era' - let era_span_length: EraNumber = - ::EraRewardSpanLength::get(); + let era_span_length: EraNumber = ::EraRewardSpanLength::get(); let era_span_index = first_claim_era - (first_claim_era % era_span_length); let era_rewards_span = pre_snapshot .era_rewards @@ -820,14 +813,14 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { // Verify post state - let post_total_issuance = ::Currency::total_issuance(); + let post_total_issuance = ::Currency::total_issuance(); assert_eq!( post_total_issuance, pre_total_issuance + total_reward, "Total issuance must increase by the total reward amount." ); - let post_free_balance = ::Currency::free_balance(&account); + let post_free_balance = ::Currency::free_balance(&account); assert_eq!( post_free_balance, pre_free_balance + total_reward, @@ -881,8 +874,7 @@ pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { return 0; }; - let era_span_length: EraNumber = - ::EraRewardSpanLength::get(); + let era_span_length: EraNumber = ::EraRewardSpanLength::get(); let first = DappStaking::era_reward_span_index(range.0) .checked_div(era_span_length) .unwrap(); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 30d34d5543..bbae432895 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -19,8 +19,8 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, - IntegratedDApps, Ledger, NextDAppId, PeriodType, StakerInfo, + pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, IntegratedDApps, + Ledger, NextDAppId, PeriodNumber, PeriodType, }; use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; @@ -142,9 +142,8 @@ fn on_initialize_state_change_works() { // Advance eras just until we reach the next voting period let eras_per_bep_period: EraNumber = - ::StandardErasPerBuildAndEarnPeriod::get(); - let blocks_per_era: BlockNumber = - ::StandardEraLength::get(); + ::StandardErasPerBuildAndEarnPeriod::get(); + let blocks_per_era: BlockNumber = ::StandardEraLength::get(); for era in 2..(2 + eras_per_bep_period - 1) { let pre_block = System::block_number(); advance_to_next_era(); @@ -205,7 +204,7 @@ fn register_already_registered_contract_fails() { #[test] fn register_past_max_number_of_contracts_fails() { ExtBuilder::build().execute_with(|| { - let limit = ::MaxNumberOfContracts::get(); + let limit = ::MaxNumberOfContracts::get(); for id in 1..=limit { assert_register(1, &MockSmartContract::Wasm(id.into())); } @@ -385,10 +384,7 @@ fn lock_is_ok() { // Ensure minimum lock amount works let locker = 3; - assert_lock( - locker, - ::MinimumLockedAmount::get(), - ); + assert_lock(locker, ::MinimumLockedAmount::get()); }) } @@ -412,8 +408,7 @@ fn lock_with_incorrect_amount_fails() { // Locking just below the minimum amount should fail let locker = 2; - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); assert_noop!( DappStaking::lock(RuntimeOrigin::signed(locker), minimum_locked_amount - 1), Error::::LockedAmountBelowThreshold, @@ -455,8 +450,7 @@ fn unlock_with_remaining_amount_below_threshold_is_ok() { advance_to_era(ActiveProtocolState::::get().era + 3); // Unlock such amount that remaining amount is below threshold, resulting in full unlock - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); let ledger = Ledger::::get(&account); assert_unlock( account, @@ -530,8 +524,7 @@ fn unlock_everything_with_active_stake_fails() { advance_to_next_era(); // We stake so the amount is just below the minimum locked amount, causing full unlock impossible. - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); let stake_amount = minimum_locked_amount - 1; // Register contract & stake on it @@ -583,7 +576,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { assert_lock(account, lock_amount); let unlock_amount = 3; - for _ in 0..::MaxUnlockingChunks::get() { + for _ in 0..::MaxUnlockingChunks::get() { run_for_blocks(1); assert_unlock(account, unlock_amount); } @@ -605,8 +598,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { #[test] fn claim_unlocked_is_ok() { ExtBuilder::build().execute_with(|| { - let unlocking_blocks: BlockNumber = - ::UnlockingPeriod::get(); + let unlocking_blocks: BlockNumber = ::UnlockingPeriod::get(); // Lock some amount in a few eras let account = 2; @@ -620,8 +612,7 @@ fn claim_unlocked_is_ok() { assert_claim_unlocked(account); // Advanced example - let max_unlocking_chunks: u32 = - ::MaxUnlockingChunks::get(); + let max_unlocking_chunks: u32 = ::MaxUnlockingChunks::get(); for _ in 0..max_unlocking_chunks { run_for_blocks(1); assert_unlock(account, unlock_amount); @@ -651,8 +642,7 @@ fn claim_unlocked_no_eligible_chunks_fails() { // Cannot claim if unlock period hasn't passed yet let lock_amount = 103; assert_lock(account, lock_amount); - let unlocking_blocks: BlockNumber = - ::UnlockingPeriod::get(); + let unlocking_blocks: BlockNumber = ::UnlockingPeriod::get(); run_for_blocks(unlocking_blocks - 1); assert_noop!( DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), @@ -677,8 +667,7 @@ fn relock_unlocking_is_ok() { assert_relock_unlocking(account); - let max_unlocking_chunks: u32 = - ::MaxUnlockingChunks::get(); + let max_unlocking_chunks: u32 = ::MaxUnlockingChunks::get(); for _ in 0..max_unlocking_chunks { run_for_blocks(1); assert_unlock(account, unlock_amount); @@ -701,8 +690,7 @@ fn relock_unlocking_no_chunks_fails() { #[test] fn relock_unlocking_insufficient_lock_amount_fails() { ExtBuilder::build().execute_with(|| { - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); // lock amount should be above the threshold let account = 2; @@ -732,8 +720,7 @@ fn relock_unlocking_insufficient_lock_amount_fails() { }); // Make sure only one chunk is left - let unlocking_blocks: BlockNumber = - ::UnlockingPeriod::get(); + let unlocking_blocks: BlockNumber = ::UnlockingPeriod::get(); run_for_blocks(unlocking_blocks - 1); assert_claim_unlocked(account); @@ -887,8 +874,7 @@ fn stake_fails_due_to_too_small_staking_amount() { assert_lock(account, 300); // Stake with too small amount, expect a failure - let min_stake_amount: Balance = - ::MinimumStakeAmount::get(); + let min_stake_amount: Balance = ::MinimumStakeAmount::get(); assert_noop!( DappStaking::stake( RuntimeOrigin::signed(account), @@ -953,8 +939,7 @@ fn unstake_with_leftover_amount_below_minimum_works() { let amount = 300; assert_lock(account, amount); - let min_stake_amount: Balance = - ::MinimumStakeAmount::get(); + let min_stake_amount: Balance = ::MinimumStakeAmount::get(); assert_stake(account, &smart_contract, min_stake_amount); // Unstake some amount, bringing it below the minimum @@ -1115,77 +1100,122 @@ fn unstake_from_past_period_fails() { }) } -// #[test] -// fn claim_staker_rewards_basic_example_is_ok() { -// ExtBuilder::build().execute_with(|| { -// // Register smart contract, lock&stake some amount -// let dev_account = 1; -// let smart_contract = MockSmartContract::default(); -// assert_register(dev_account, &smart_contract); - -// let account = 2; -// let lock_amount = 300; -// assert_lock(account, lock_amount); -// let stake_amount = 93; -// assert_stake(account, &smart_contract, stake_amount); - -// // Advance into Build&Earn period, and allow one era to pass. Claim reward for 1 era. -// advance_to_era(ActiveProtocolState::::get().era + 2); -// assert_claim_staker_rewards(account); - -// // Advance a few more eras, and claim multiple rewards this time. -// advance_to_era(ActiveProtocolState::::get().era + 3); -// assert_eq!( -// ActiveProtocolState::::get().period_number(), -// 1, -// "Sanity check, we must still be in the 1st period." -// ); -// assert_claim_staker_rewards(account); - -// // Advance into the next period, make sure we can still claim old rewards. -// advance_to_next_period(); -// for _ in 0..required_number_of_reward_claims(account) { -// assert_claim_staker_rewards(account); -// } -// }) -// } - -// #[test] -// fn claim_staker_rewards_no_claimable_rewards_fails() { -// ExtBuilder::build().execute_with(|| { -// // Register smart contract, lock&stake some amount -// let dev_account = 1; -// let smart_contract = MockSmartContract::default(); -// assert_register(dev_account, &smart_contract); - -// let account = 2; -// let lock_amount = 300; -// assert_lock(account, lock_amount); - -// // 1st scenario - try to claim with no stake at all. -// assert_noop!( -// DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), -// Error::::NoClaimableRewards, -// ); - -// // 2nd scenario - stake some amount, and try to claim in the same era. -// // It's important this is the 1st era, when no `EraRewards` entry exists. -// assert_eq!(ActiveProtocolState::::get().era, 1, "Sanity check"); -// assert!(EraRewards::::iter().next().is_none(), "Sanity check"); -// let stake_amount = 93; -// assert_stake(account, &smart_contract, stake_amount); -// assert_noop!( -// DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), -// Error::::NoClaimableRewards, -// ); - -// // 3rd scenario - move over to the next era, but we still expect failure because -// // stake is valid from era 2 (current era), and we're trying to claim rewards for era 1. -// advance_to_next_era(); -// assert!(EraRewards::::iter().next().is_some(), "Sanity check"); -// assert_noop!( -// DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), -// Error::::NoClaimableRewards, -// ); -// }) -// } +#[test] +fn claim_staker_rewards_basic_example_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance into Build&Earn period, and allow one era to pass. Claim reward for 1 era. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_claim_staker_rewards(account); + + // Advance a few more eras, and claim multiple rewards this time. + advance_to_era(ActiveProtocolState::::get().era + 3); + assert_eq!( + ActiveProtocolState::::get().period_number(), + 1, + "Sanity check, we must still be in the 1st period." + ); + assert_claim_staker_rewards(account); + + // Advance into the next period, make sure we can still claim old rewards. + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + }) +} + +#[test] +fn claim_staker_rewards_no_claimable_rewards_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // 1st scenario - try to claim with no stake at all. + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 2nd scenario - stake some amount, and try to claim in the same era. + // It's important this is the 1st era, when no `EraRewards` entry exists. + assert_eq!(ActiveProtocolState::::get().era, 1, "Sanity check"); + assert!(EraRewards::::iter().next().is_none(), "Sanity check"); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 3rd scenario - move over to the next era, but we still expect failure because + // stake is valid from era 2 (current era), and we're trying to claim rewards for era 1. + advance_to_next_era(); + assert!(EraRewards::::iter().next().is_some(), "Sanity check"); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_staker_rewards_after_expiry_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + + // Advance to the block just before the 'expiry' period starts + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods, + ); + advance_to_next_period_type(); + advance_to_era(ActiveProtocolState::::get().period_info.ending_era - 1); + assert_claim_staker_rewards(account); + + // Ensure we're still in the first period for the sake of test validity + assert_eq!( + Ledger::::get(&account).staked.period, + 1, + "Sanity check." + ); + + // Trigger next period, rewards should be marked as expired + advance_to_next_era(); + assert_eq!( + ActiveProtocolState::::get().period_number(), + reward_retention_in_periods + 2 + ); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::StakerRewardsExpired, + ); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 5a7399e4ea..f92cca8b8f 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -438,7 +438,6 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { assert!(acc_ledger .add_stake_amount(stake_amount, first_era, period_info_1) .is_ok()); - let acc_ledger_snapshot = acc_ledger.clone(); // Try to add to the next era, it should fail. assert_eq!( diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 907af2eb30..345f4c613c 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -537,10 +537,12 @@ where self.staked .subtract(amount, current_period_info.period_type); + // Convenience cleanup if self.staked.is_empty() { self.staked = Default::default(); } + if let Some(mut stake_amount) = self.staked_future { stake_amount.subtract(amount, current_period_info.period_type); @@ -586,7 +588,7 @@ where return Err(AccountLedgerError::NothingToClaim); } else if let Some(stake_amount) = self.staked_future { // Future entry exists, but era isn't 'in history' - if era <= stake_amount.era { + if era < stake_amount.era { return Err(AccountLedgerError::NothingToClaim); } } @@ -610,8 +612,11 @@ where let result = RewardsIter::new(span, maybe_other); - // Update latest 'staked' era - self.staked.era = era; + // Rollover future to 'current' stake amount + if let Some(stake_amount) = self.staked_future.take() { + self.staked = stake_amount; + } + self.staked.era = era.saturating_add(1); // Make sure to clean up the entries match period_end { From 80a0e4f37ea47555c1fe2990926b2db11fb944f1 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 18 Oct 2023 14:22:16 +0200 Subject: [PATCH 11/86] Bonus rewards --- pallets/dapp-staking-v3/src/lib.rs | 93 ++++++++++++++++++- .../dapp-staking-v3/src/test/testing_utils.rs | 56 ++++++++++- pallets/dapp-staking-v3/src/types.rs | 69 ++++++++++++-- 3 files changed, 205 insertions(+), 13 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 3e3eef7acd..1fd47435d5 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -206,6 +206,11 @@ pub mod pallet { era: EraNumber, amount: Balance, }, + BonusReward { + account: T::AccountId, + period: PeriodNumber, + amount: Balance, + }, } #[pallet::error] @@ -259,8 +264,14 @@ pub mod pallet { StakerRewardsExpired, /// There are no claimable rewards for the account. NoClaimableRewards, - /// An unexpected error occured while trying to claim rewards. + /// An unexpected error occured while trying to claim staker rewards. InternalClaimStakerError, + /// Bonus rewards have already been claimed. + BonusRewardAlreadyClaimed, + /// Account is has no eligible stake amount for bonus reward. + NotEligibleForBonusReward, + /// An unexpected error occured while trying to claim bonus reward. + InternalClaimBonusError, } /// General information about dApp staking protocol state. @@ -1011,7 +1022,11 @@ pub mod pallet { // TODO: how do we handle expired rewards? Add an additional call to clean them up? // Putting this logic inside existing calls will add even more complexity. - // Check if the rewards have expired + ensure!( + !ledger.staker_rewards_claimed, + Error::::NoClaimableRewards + ); // TODO: maybe different error type? + // Check if the rewards have expired let staked_period = ledger .staked_period() .ok_or(Error::::NoClaimableRewards)?; @@ -1073,7 +1088,7 @@ pub mod pallet { reward_sum.saturating_accrue(staker_reward); } - // TODO: add negative test for this. + // TODO: add negative test for this? T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; @@ -1089,6 +1104,78 @@ pub mod pallet { Ok(()) } + + /// TODO + #[pallet::call_index(12)] + #[pallet::weight(Weight::zero())] + pub fn claim_bonus_reward(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let protocol_state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + ensure!( + !ledger.staker_rewards_claimed, + Error::::BonusRewardAlreadyClaimed + ); + // Check if the rewards have expired + let staked_period = ledger + .staked_period() + .ok_or(Error::::NoClaimableRewards)?; + ensure!( + staked_period + >= protocol_state + .period_number() + .saturating_sub(T::RewardRetentionInPeriods::get()), + Error::::StakerRewardsExpired + ); + + // Check if period has ended + ensure!( + staked_period < protocol_state.period_number(), + Error::::NoClaimableRewards + ); + + // Check if user is applicable for bonus reward + let eligible_amount = + ledger + .claim_bonus_reward(staked_period) + .map_err(|err| match err { + AccountLedgerError::NothingToClaim => Error::::NoClaimableRewards, + _ => Error::::InternalClaimBonusError, + })?; + ensure!( + !eligible_amount.is_zero(), + Error::::NotEligibleForBonusReward + ); + + let period_end_info = + PeriodEnd::::get(&staked_period).ok_or(Error::::InternalClaimBonusError)?; + // Defensive check, situation should never happen. + ensure!( + !period_end_info.total_vp_stake.is_zero(), + Error::::InternalClaimBonusError + ); + + let bonus_reward = + Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) + * period_end_info.bonus_reward_pool; + + // TODO: add negative test for this? + T::Currency::deposit_into_existing(&account, bonus_reward) + .map_err(|_| Error::::InternalClaimStakerError)?; + + Self::update_ledger(&account, ledger); + + Self::deposit_event(Event::::BonusReward { + account: account.clone(), + period: staked_period, + amount: bonus_reward, + }); + + Ok(()) + } } impl Pallet { diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 1c328f852b..2884feeed8 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -831,18 +831,68 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let post_ledger = post_snapshot.ledger.get(&account).unwrap(); if is_full_claim { - assert!(post_ledger.staked.is_empty()); - assert!(post_ledger.staked_future.is_none()); + assert!(post_ledger.staker_rewards_claimed); } else { assert_eq!(post_ledger.staked.era, last_claim_era + 1); // TODO: expand check? } } +/// Claim staker rewards. +pub(crate) fn assert_claim_bonus_reward(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(&account); + + let staked_period = pre_ledger + .staked_period() + .expect("Must have a staked period."); + let stake_amount = pre_ledger.staked_amount_for_type(PeriodType::Voting, staked_period); + + let period_end_info = pre_snapshot + .period_end + .get(&staked_period) + .expect("Entry must exist, since it's a past period."); + + let reward = Perbill::from_rational(stake_amount, period_end_info.total_vp_stake) + * period_end_info.bonus_reward_pool; + + // Unstake from smart contract & verify event(s) + assert_ok!(DappStaking::claim_bonus_reward(RuntimeOrigin::signed( + account + ),)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::BonusReward { + account, + period: staked_period, + amount: reward, + })); + + // Verify post state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + reward, + "Total issuance must increase by the reward amount." + ); + + let post_free_balance = ::Currency::free_balance(&account); + assert_eq!( + post_free_balance, + pre_free_balance + reward, + "Free balance must increase by the reward amount." + ); + + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); +} + /// Returns from which starting era to which ending era can rewards be claimed for the specified account. /// /// If `None` is returned, there is nothing to claim. -/// Doesn't consider reward expiration. +/// +/// **NOTE:** Doesn't consider reward expiration. pub(crate) fn claimable_reward_range(account: AccountId) -> Option<(EraNumber, EraNumber)> { let ledger = Ledger::::get(&account); let protocol_state = ActiveProtocolState::::get(); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 345f4c613c..9727677e5b 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . +// TODO: docs + use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; @@ -53,6 +55,8 @@ pub enum AccountLedgerError { UnstakeAmountLargerThanStake, /// Nothing to claim. NothingToClaim, + /// Rewards have already been claimed + AlreadyClaimed, } // TODO: rename to SubperiodType? It would be less ambigious. @@ -418,6 +422,7 @@ where } } + // TODO pub fn staked_amount_for_type( &self, period_type: PeriodType, @@ -556,7 +561,7 @@ where Ok(()) } - // TODO + /// Period for which account has staking information or `None` if no staking information exists. pub fn staked_period(&self) -> Option { if self.staked.is_empty() { self.staked_future.map(|stake_amount| stake_amount.period) @@ -565,7 +570,7 @@ where } } - // TODO + /// Earliest era for which the account has staking information or `None` if no staking information exists. pub fn earliest_staked_era(&self) -> Option { if self.staked.is_empty() { self.staked_future.map(|stake_amount| stake_amount.era) @@ -574,8 +579,9 @@ where } } - /// Claim up stake chunks up to the specified `era`. - /// Returns the vector describing claimable chunks. + /// 'Claim' rewards up to the specified era. + /// Returns an iterator over the `(era, amount)` pairs, where `amount` + /// describes the staked amount eligible for reward in the appropriate era. /// /// If `period_end` is provided, it's used to determine whether all applicable chunks have been claimed. pub fn claim_up_to_era( @@ -583,6 +589,10 @@ where era: EraNumber, period_end: Option, ) -> Result { + if self.staker_rewards_claimed { + return Err(AccountLedgerError::AlreadyClaimed); + } + // Main entry exists, but era isn't 'in history' if !self.staked.is_empty() && era <= self.staked.era { return Err(AccountLedgerError::NothingToClaim); @@ -622,15 +632,60 @@ where match period_end { Some(ending_era) if era >= ending_era => { self.staker_rewards_claimed = true; - // TODO: probably not the right thing to do, need to check if pending bonus rewards need to be claimed as well - self.staked = Default::default(); - self.staked_future = None; + self.maybe_cleanup_stake_entries(); } _ => (), } Ok(result) } + + /// Claim bonus reward, if possible. + /// + /// Returns the amount eligible for bonus reward calculation, or an error. + pub fn claim_bonus_reward( + &mut self, + period: PeriodNumber, + ) -> Result { + if self.bonus_reward_claimed { + return Err(AccountLedgerError::AlreadyClaimed); + } + + let amount = self.staked_amount_for_type(PeriodType::Voting, period); + + if amount.is_zero() { + Err(AccountLedgerError::NothingToClaim) + } else { + self.bonus_reward_claimed = true; + self.maybe_cleanup_stake_entries(); + Ok(amount) + } + } + + /// Cleanup fields related to stake & rewards, in case all possible rewards for the current state + /// have been claimed. + fn maybe_cleanup_stake_entries(&mut self) { + // Stake rewards are either all claimed, or there's nothing to claim. + let stake_cleanup = + self.staker_rewards_claimed || (self.staked.is_empty() && self.staked_future.is_none()); + + // Bonus reward might be covered by the 'future' entry. + let has_future_entry_stake = if let Some(stake_amount) = self.staked_future { + !stake_amount.voting.is_zero() + } else { + false + }; + // Either rewards have already been claimed, or there are no possible bonus rewards to claim. + let bonus_cleanup = + self.bonus_reward_claimed || self.staked.voting.is_zero() && !has_future_entry_stake; + + if stake_cleanup && bonus_cleanup { + self.staked = Default::default(); + self.staked_future = None; + self.staker_rewards_claimed = false; + self.bonus_reward_claimed = false; + } + } } // TODO: docs & test From 82c71ad2077d59810ce9eabe399bfd6dd8d44eb8 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 18 Oct 2023 14:49:14 +0200 Subject: [PATCH 12/86] Docs & some minor changes --- .../dapp-staking-v3/src/test/testing_utils.rs | 7 +- pallets/dapp-staking-v3/src/types.rs | 102 +++++++++++------- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 2884feeed8..48abb371bb 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -529,8 +529,11 @@ pub(crate) fn assert_stake( "Staked amount must increase by the 'amount'" ); - assert_eq!(post_contract_stake.last_stake_period(), Some(stake_period)); - assert_eq!(post_contract_stake.last_stake_era(), Some(stake_era)); + assert_eq!( + post_contract_stake.latest_stake_period(), + Some(stake_period) + ); + assert_eq!(post_contract_stake.latest_stake_era(), Some(stake_era)); // 4. verify era info // ========================= diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 9727677e5b..1df13ffb8a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -78,12 +78,15 @@ impl PeriodType { } } -/// Wrapper type around current `PeriodType` and era number when it's expected to end. +/// Info about the ongoing period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct PeriodInfo { + /// Period number. Increments after each build&earn period type. #[codec(compact)] pub number: PeriodNumber, + /// Subperiod type. pub period_type: PeriodType, + /// Last ear of the sub-period, after this a new sub-period should start. #[codec(compact)] pub ending_era: EraNumber, } @@ -105,13 +108,16 @@ impl PeriodInfo { } } -// TODO: doc +/// Information describing relevant information for a finished period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct PeriodEndInfo { + /// Bonus reward pool allocated for 'loyal' stakers #[codec(compact)] pub bonus_reward_pool: Balance, + /// Total amount staked (remaining) from the voting period. #[codec(compact)] pub total_vp_stake: Balance, + /// Final era, inclusive, in which the period ended. #[codec(compact)] pub final_era: EraNumber, } @@ -132,14 +138,11 @@ pub struct ProtocolState { #[codec(compact)] pub era: EraNumber, /// Block number at which the next era should start. - /// TODO: instead of abusing on-initialize and wasting block-space, - /// I believe we should utilize `pallet-scheduler` to schedule the next era. Make an item for this. #[codec(compact)] pub next_era_start: BlockNumber, - /// Ongoing period type and when is it expected to end. + /// Information about the ongoing period. pub period_info: PeriodInfo, /// `true` if pallet is in maintenance mode (disabled), `false` otherwise. - /// TODO: provide some configurable barrier to handle this on the runtime level instead? Make an item for this? pub maintenance: bool, } @@ -165,7 +168,7 @@ impl ProtocolState where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, { - /// Current period type. + /// Current sub-period type. pub fn period_type(&self) -> PeriodType { self.period_info.period_type } @@ -230,8 +233,10 @@ pub struct DAppInfo { /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct UnlockingChunk { + /// Amount undergoing the unlocking period. #[codec(compact)] pub amount: Balance, + /// Block in which the unlocking period is finished. #[codec(compact)] pub unlock_block: BlockNumber, } @@ -257,16 +262,19 @@ pub struct AccountLedger< > { /// How much active locked amount an account has. pub locked: Balance, - /// How much started unlocking on a certain block + /// Vector of all the unlocking chunks. pub unlocking: BoundedVec, UnlockingLen>, - /// How much user has/had staked in a particular era. + /// Primary field used to store how much was staked in a particular era. pub staked: StakeAmount, - /// Helper staked amount to keep track of future era stakes. + /// Secondary field used to store 'stake' information for the 'next era'. + /// This is needed since stake amount is only applicable from the next era after it's been staked. + /// /// Both `stake` and `staked_future` must ALWAYS refer to the same period. + /// If `staked_future` is `Some`, it will always be **EXACTLY** one era after the `staked` field era. pub staked_future: Option, - /// TODO + /// Indicator whether staker rewards for the entire period have been claimed. pub staker_rewards_claimed: bool, - /// TODO + /// Indicator whether bonus rewards for the period has been claimed. pub bonus_reward_claimed: bool, } @@ -422,21 +430,16 @@ where } } - // TODO - pub fn staked_amount_for_type( - &self, - period_type: PeriodType, - active_period: PeriodNumber, - ) -> Balance { + /// How much is staked for the specified period type, in respect to the specified era. + // TODO: if period is force-ended, this could be abused to get bigger rewards. I need to make the check more strict when claiming bonus rewards! + pub fn staked_amount_for_type(&self, period_type: PeriodType, period: PeriodNumber) -> Balance { // First check the 'future' entry, afterwards check the 'first' entry match self.staked_future { - Some(stake_amount) if stake_amount.period == active_period => { + Some(stake_amount) if stake_amount.period == period => { stake_amount.for_type(period_type) } _ => match self.staked { - stake_amount if stake_amount.period == active_period => { - stake_amount.for_type(period_type) - } + stake_amount if stake_amount.period == period => stake_amount.for_type(period_type), _ => Balance::zero(), }, } @@ -445,9 +448,13 @@ where // TODO: update this /// Adds the specified amount to total staked amount, if possible. /// - /// Staking is only allowed if one of the two following conditions is met: - /// 1. Staker is staking again in the period in which they already staked. - /// 2. Staker is staking for the first time in this period, and there are no staking chunks from the previous eras. + /// Staking can only be done for the ongoing period, and era. + /// 1. The `period` requirement enforces staking in the ongoing period. + /// 2. The `era` requirement enforces staking in the ongoing era. + /// + /// The 2nd condition is needed to prevent stakers from building a significant histort of stakes, + /// without claiming the rewards. So if a historic era exists as an entry, stakers will first need to claim + /// the pending rewards, before they can stake again. /// /// Additonally, the staked amount must not exceed what's available for staking. pub fn add_stake_amount( @@ -460,7 +467,6 @@ where return Ok(()); } - // TODO: maybe the check can be nicer? if !self.staked.is_empty() { // In case entry for the current era exists, it must match the era exactly. if self.staked.era != era { @@ -503,8 +509,9 @@ where /// Subtracts the specified amount from the total staked amount, if possible. /// - /// Unstaking will reduce total stake for the current era, and next era(s). - /// The specified amount must not exceed what's available for staking. + /// Unstake can only be called if the entry for the current era exists. + /// In case historic entry exists, rewards first need to be claimed, before unstaking is possible. + /// Similar as with stake functionality, this is to prevent staker from building a significant history of stakes. pub fn unstake_amount( &mut self, amount: Balance, @@ -515,7 +522,7 @@ where return Ok(()); } - // TODO: maybe the check can be nicer? (and not duplicated?) + // TODO: this is a duplicated check, maybe I should extract it into a function? if !self.staked.is_empty() { // In case entry for the current era exists, it must match the era exactly. if self.staked.era != era { @@ -588,7 +595,7 @@ where &mut self, era: EraNumber, period_end: Option, - ) -> Result { + ) -> Result { if self.staker_rewards_claimed { return Err(AccountLedgerError::AlreadyClaimed); } @@ -620,7 +627,7 @@ where ((self.staked.era, era, self.staked.total()), None) }; - let result = RewardsIter::new(span, maybe_other); + let result = EraStakePairIter::new(span, maybe_other); // Rollover future to 'current' stake amount if let Some(stake_amount) = self.staked_future.take() { @@ -688,16 +695,30 @@ where } } -// TODO: docs & test +/// Helper internal struct for iterating over `(era, stake amount)` pairs. +/// +/// Due to how `AccountLedger` is implemented, few scenarios are possible when claming rewards: +/// +/// 1. `staked` has some amount, `staked_future` is `None` +/// * `maybe_other` is `None`, span describes the entire range +/// 2. `staked` has nothing, `staked_future` is some and has some amount +/// * `maybe_other` is `None`, span describes the entire range +/// 3. `staked` has some amount, `staked_future` has some amount +/// * `maybe_other` is `Some` and covers the `staked` entry, span describes the entire range except the first pair. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct RewardsIter { +pub struct EraStakePairIter { + /// Denotes whether the first entry is different than the others. maybe_other: Option<(EraNumber, Balance)>, + /// Starting era of the span. start_era: EraNumber, + /// Ending era of the span. end_era: EraNumber, + /// Staked amount in the span. amount: Balance, } -impl RewardsIter { +impl EraStakePairIter { + /// Create new iterator struct for `(era, staked amount)` pairs. pub fn new( span: (EraNumber, EraNumber, Balance), maybe_other: Option<(EraNumber, Balance)>, @@ -718,7 +739,7 @@ impl RewardsIter { } } -impl Iterator for RewardsIter { +impl Iterator for EraStakePairIter { type Item = (EraNumber, Balance); fn next(&mut self) -> Option { @@ -738,7 +759,7 @@ impl Iterator for RewardsIter { } } -// TODO +/// Describes stake amount in an particular era/period. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct StakeAmount { /// Amount of staked funds accounting for the voting period. @@ -771,6 +792,7 @@ impl StakeAmount { } } + /// `true` if nothing is stked, `false` otherwise pub fn is_empty(&self) -> bool { self.voting.is_zero() && self.build_and_earn.is_zero() } @@ -1019,13 +1041,13 @@ impl ContractStakeAmountSeries { self.0.is_empty() } - // TODO - pub fn last_stake_period(&self) -> Option { + /// Latest period for which stake entry exists. + pub fn latest_stake_period(&self) -> Option { self.0.last().map(|x| x.period) } - // TODO - pub fn last_stake_era(&self) -> Option { + /// Latest era for which stake entry exists. + pub fn latest_stake_era(&self) -> Option { self.0.last().map(|x| x.era) } From 582aef4649683ab3dde0787ee97999c9962f4de3 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 18 Oct 2023 17:44:42 +0200 Subject: [PATCH 13/86] Comments, tests, improved coverage --- pallets/dapp-staking-v3/src/lib.rs | 35 ++-- .../dapp-staking-v3/src/test/testing_utils.rs | 24 ++- pallets/dapp-staking-v3/src/test/tests.rs | 189 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 4 +- 4 files changed, 231 insertions(+), 21 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 1fd47435d5..01a9560a71 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -261,7 +261,9 @@ pub mod pallet { /// An unexpected error occured while trying to unstake. InternalUnstakeError, /// Rewards are no longer claimable since they are too old. - StakerRewardsExpired, + RewardExpired, + /// All staker rewards for the period have been claimed. + StakerRewardsAlreadyClaimed, /// There are no claimable rewards for the account. NoClaimableRewards, /// An unexpected error occured while trying to claim staker rewards. @@ -964,6 +966,7 @@ pub mod pallet { ledger .unstake_amount(amount, unstake_era, protocol_state.period_info) .map_err(|err| match err { + // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewardsFromPastPeriods } @@ -1024,9 +1027,10 @@ pub mod pallet { ensure!( !ledger.staker_rewards_claimed, - Error::::NoClaimableRewards - ); // TODO: maybe different error type? - // Check if the rewards have expired + Error::::StakerRewardsAlreadyClaimed + ); + + // Check if the rewards have expired let staked_period = ledger .staked_period() .ok_or(Error::::NoClaimableRewards)?; @@ -1035,7 +1039,7 @@ pub mod pallet { >= protocol_state .period_number() .saturating_sub(T::RewardRetentionInPeriods::get()), - Error::::StakerRewardsExpired + Error::::RewardExpired ); // Calculate the reward claim span @@ -1088,7 +1092,6 @@ pub mod pallet { reward_sum.saturating_accrue(staker_reward); } - // TODO: add negative test for this? T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; @@ -1116,7 +1119,7 @@ pub mod pallet { let mut ledger = Ledger::::get(&account); ensure!( - !ledger.staker_rewards_claimed, + !ledger.bonus_reward_claimed, Error::::BonusRewardAlreadyClaimed ); // Check if the rewards have expired @@ -1128,7 +1131,7 @@ pub mod pallet { >= protocol_state .period_number() .saturating_sub(T::RewardRetentionInPeriods::get()), - Error::::StakerRewardsExpired + Error::::RewardExpired ); // Check if period has ended @@ -1145,24 +1148,28 @@ pub mod pallet { AccountLedgerError::NothingToClaim => Error::::NoClaimableRewards, _ => Error::::InternalClaimBonusError, })?; - ensure!( - !eligible_amount.is_zero(), - Error::::NotEligibleForBonusReward - ); let period_end_info = PeriodEnd::::get(&staked_period).ok_or(Error::::InternalClaimBonusError)?; - // Defensive check, situation should never happen. + + // Defensive check - we should never get this far in function if no voting period stake exists. ensure!( !period_end_info.total_vp_stake.is_zero(), Error::::InternalClaimBonusError ); + // TODO: this functionality is incomplete - what we should do is iterate over all stake entries in the storage, + // and check if the smart contract was still registered when period ended. + // + // This is important since we cannot allow unregistered contracts to be subject for bonus rewards. + // This means 'a loop' but it will be bounded by max limit of unique stakes. + // A function should also be introduced to prepare the account ledger for next era (or to cleanup old expired rewards) + // in case bonus rewards weren't claimed. + let bonus_reward = Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; - // TODO: add negative test for this? T::Currency::deposit_into_existing(&account, bonus_reward) .map_err(|_| Error::::InternalClaimStakerError)?; diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 48abb371bb..644a71f6e9 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -796,7 +796,7 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { //clean up possible leftover events System::reset_events(); - // Unstake from smart contract & verify event(s) + // Claim staker rewards & verify all events assert_ok!(DappStaking::claim_staker_rewards(RuntimeOrigin::signed( account ),)); @@ -833,11 +833,16 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let post_snapshot = MemorySnapshot::new(); let post_ledger = post_snapshot.ledger.get(&account).unwrap(); - if is_full_claim { + if is_full_claim && pre_ledger.bonus_reward_claimed { + assert_eq!(post_ledger.staked, StakeAmount::default()); + assert!(post_ledger.staked_future.is_none()); + assert!(!post_ledger.staker_rewards_claimed); + assert!(!post_ledger.bonus_reward_claimed); + } else if is_full_claim { assert!(post_ledger.staker_rewards_claimed); } else { assert_eq!(post_ledger.staked.era, last_claim_era + 1); - // TODO: expand check? + assert!(post_ledger.staked_future.is_none()); } } @@ -861,7 +866,7 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId) { let reward = Perbill::from_rational(stake_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; - // Unstake from smart contract & verify event(s) + // Claim bonus reward & verify event assert_ok!(DappStaking::claim_bonus_reward(RuntimeOrigin::signed( account ),)); @@ -889,6 +894,17 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId) { let post_snapshot = MemorySnapshot::new(); let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + + // All rewards for period have been claimed + if pre_ledger.staker_rewards_claimed { + assert_eq!(post_ledger.staked, StakeAmount::default()); + assert!(post_ledger.staked_future.is_none()); + assert!(!post_ledger.staker_rewards_claimed); + assert!(!post_ledger.bonus_reward_claimed); + } else { + // Staker still has some staker rewards remaining + assert!(post_ledger.bonus_reward_claimed); + } } /// Returns from which starting era to which ending era can rewards be claimed for the specified account. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index bbae432895..87716c6c5f 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -626,6 +626,12 @@ fn claim_unlocked_is_ok() { run_for_blocks(2); assert_claim_unlocked(account); assert!(Ledger::::get(&account).unlocking.is_empty()); + + // Unlock everything + assert_unlock(account, lock_amount); + run_for_blocks(unlocking_blocks); + assert_claim_unlocked(account); + assert!(!Ledger::::contains_key(&account)); }) } @@ -1135,6 +1141,33 @@ fn claim_staker_rewards_basic_example_is_ok() { }) } +#[test] +fn claim_staker_rewards_double_call_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance into the next period, claim all eligible rewards + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::StakerRewardsAlreadyClaimed, + ); + }) +} + #[test] fn claim_staker_rewards_no_claimable_rewards_fails() { ExtBuilder::build().execute_with(|| { @@ -1215,7 +1248,161 @@ fn claim_staker_rewards_after_expiry_fails() { ); assert_noop!( DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), - Error::::StakerRewardsExpired, + Error::::RewardExpired, + ); + }) +} + +#[test] +fn claim_bonus_reward_works() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // 1st scenario - advance to the next period, first claim bonus reward, then staker rewards + advance_to_next_period(); + assert_claim_bonus_reward(account); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // 2nd scenario - stake again, advance to next period, this time first claim staker rewards, then bonus reward. + assert_stake(account, &smart_contract, stake_amount); + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert!( + Ledger::::get(&account).staker_rewards_claimed, + "Sanity check." + ); + assert_claim_bonus_reward(account); + }) +} + +#[test] +fn claim_bonus_reward_double_call_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance to the next period, claim bonus reward, then try to do it again + advance_to_next_period(); + assert_claim_bonus_reward(account); + + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + Error::::BonusRewardAlreadyClaimed, + ); + }) +} + +#[test] +fn claim_bonus_reward_when_nothing_to_claim_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // 1st - try to claim bonus reward when no stake is present + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 2nd - try to claim bonus reward for the ongoing period + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // Stake in Build&Earn period type, advance to next era and try to claim bonus reward + advance_to_next_period_type(); + assert_eq!( + ActiveProtocolState::::get().period_type(), + PeriodType::BuildAndEarn, + "Sanity check." + ); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + advance_to_next_period(); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_bonus_reward_after_expiry_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + assert_stake(account, &smart_contract, lock_amount); + + // 1st scenario - Advance to one period before the expiry, claim should still work. + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods, + ); + assert_claim_bonus_reward(account); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // 2nd scenario - advance past the expiry, call must fail + assert_stake(account, &smart_contract, lock_amount); + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods + 1, + ); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + Error::::RewardExpired, ); }) } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 1df13ffb8a..8065b35c90 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -276,6 +276,7 @@ pub struct AccountLedger< pub staker_rewards_claimed: bool, /// Indicator whether bonus rewards for the period has been claimed. pub bonus_reward_claimed: bool, + // TODO: introduce a variable which keeps track of on how many contracts this account has stake entries for. } impl Default for AccountLedger @@ -445,7 +446,6 @@ where } } - // TODO: update this /// Adds the specified amount to total staked amount, if possible. /// /// Staking can only be done for the ongoing period, and era. @@ -1225,7 +1225,7 @@ impl ContractStakeAmountSeries { fn prune(&mut self) { // Prune the oldest entry if we have more than the limit if self.0.len() == STAKING_SERIES_HISTORY as usize { - // TODO: this can be perhaps optimized so we prune entries which are very old. + // This can be perhaps optimized so we prune entries which are very old. // However, this makes the code more complex & more error prone. // If kept like this, we always make sure we cover the history, and we never exceed it. self.0.remove(0); From b25ccb10dc4b1e2b4719a87be78147cd161188ef Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 20 Oct 2023 11:40:08 +0200 Subject: [PATCH 14/86] Tier params & config init solution --- pallets/dapp-staking-v3/src/lib.rs | 6 +- .../dapp-staking-v3/src/test/tests_types.rs | 48 ++++++ pallets/dapp-staking-v3/src/types.rs | 151 +++++++++++++++++- 3 files changed, 201 insertions(+), 4 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 01a9560a71..348a101071 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1012,7 +1012,8 @@ pub mod pallet { Ok(()) } - /// TODO + // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. + /// TODO: docs #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { @@ -1108,7 +1109,8 @@ pub mod pallet { Ok(()) } - /// TODO + // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. + /// TODO: documentation #[pallet::call_index(12)] #[pallet::weight(Weight::zero())] pub fn claim_bonus_reward(origin: OriginFor) -> DispatchResult { diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index f92cca8b8f..0eb4544dc1 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -17,6 +17,8 @@ // along with Astar. If not, see . use frame_support::assert_ok; +use sp_arithmetic::fixed_point::FixedU64; +use sp_runtime::Permill; use crate::test::mock::{Balance, *}; use crate::*; @@ -1514,3 +1516,49 @@ fn era_reward_span_fails_when_expected() { Err(EraRewardSpanError::NoCapacity) ); } + +#[test] +fn tier_slot_configuration_basic_tests() { + // TODO: this should be expanded & improved later + get_u32_type!(TiersNum, 4); + let params = TierSlotParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { amount: 1000 }, + TierThreshold::DynamicTvlAmount { amount: 500 }, + TierThreshold::DynamicTvlAmount { amount: 100 }, + TierThreshold::FixedTvlAmount { amount: 50 }, + ]) + .unwrap(), + }; + assert!(params.is_valid(), "Example params must be valid!"); + + // Create a configuration with some values + let init_config = TierSlotConfiguration:: { + number_of_slots: 100, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: params.reward_portion.clone(), + tier_thresholds: params.tier_thresholds.clone(), + }; + assert!(init_config.is_valid(), "Init config must be valid!"); + + // Create a new config, based on a new price + let new_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(new_price, ¶ms); + assert!(new_config.is_valid()); + + // TODO: expand tests, add more sanity checks (e.g. tier 3 requirement should never be lower than tier 4, etc.) +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 8065b35c90..20a98e41e8 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -21,9 +21,10 @@ use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; +use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, Zero}, - Saturating, + traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, + FixedPointNumber, Permill, Saturating, }; use astar_primitives::Balance; @@ -1357,3 +1358,149 @@ where } } } + +/// Description of tier entry requirement. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub enum TierThreshold { + /// Entry into tier is mandated by minimum amount of staked funds. + /// Value is fixed, and is not expected to change in between periods. + FixedTvlAmount { amount: Balance }, + /// Entry into tier is mandated by minimum amount of staked funds. + /// Value is expected to dynamically change in-between periods, depending on the system parameters. + DynamicTvlAmount { amount: Balance }, + // TODO: perhaps add a type that is dynamic but has lower bound below which the value cannot go? + // Otherwise we could allow e.g. tier 3 to go below tier 4, which doesn't make sense. +} + +/// 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))] +pub struct TierSlotParameters> { + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub reward_portion: BoundedVec, + /// Distribution of number of slots per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub slot_distribution: BoundedVec, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec, +} + +impl> TierSlotParameters { + /// Check if configuration is valid. + /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`. + pub fn is_valid(&self) -> bool { + let number_of_tiers: usize = NT::get() as usize; + number_of_tiers == self.reward_portion.len() + && number_of_tiers == self.slot_distribution.len() + && number_of_tiers == self.tier_thresholds.len() + } +} + +/// Configuration of dApp tiers. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(NT))] +pub struct TierSlotConfiguration> { + /// Total number of slots. + #[codec(compact)] + pub number_of_slots: u16, + /// Number of slots per tier. + /// First entry refers to the first tier, and so on. + pub slots_per_tier: BoundedVec, + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub reward_portion: BoundedVec, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec, +} + +impl> TierSlotConfiguration { + /// Check if parameters are valid. + pub fn is_valid(&self) -> bool { + let number_of_tiers: usize = NT::get() as usize; + number_of_tiers == self.slots_per_tier.len() + // All vecs length must match number of tiers. + && number_of_tiers == self.reward_portion.len() + && number_of_tiers == self.tier_thresholds.len() + // Total number of slots must match the sum of slots per tier. + && self.slots_per_tier.iter().fold(0, |acc, x| acc + x) == self.number_of_slots + } + + /// TODO + pub fn calculate_new(&self, native_price: FixedU64, params: &TierSlotParameters) -> Self { + let new_number_of_slots = Self::calculate_number_of_slots(native_price); + + // TODO: ugly, unsafe, refactor later + let new_slots_per_tier: Vec = params + .slot_distribution + .clone() + .into_inner() + .iter() + .map(|x| *x * 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. + 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(), + new_number_of_slots.into(), + ); + + let mut new_tier_thresholds = self.tier_thresholds.clone(); + new_tier_thresholds + .iter_mut() + .for_each(|threshold| match threshold { + TierThreshold::DynamicTvlAmount { amount } => { + *amount = amount + .saturating_sub(delta_threshold_decrease.saturating_mul_int(*amount)); + } + _ => (), + }); + + new_tier_thresholds + } else if new_number_of_slots < self.number_of_slots { + let delta_threshold_increase = FixedU64::from_rational( + (self.number_of_slots - new_number_of_slots).into(), + new_number_of_slots.into(), + ); + + let mut new_tier_thresholds = self.tier_thresholds.clone(); + new_tier_thresholds + .iter_mut() + .for_each(|threshold| match threshold { + TierThreshold::DynamicTvlAmount { amount } => { + *amount = amount + .saturating_add(delta_threshold_increase.saturating_mul_int(*amount)); + } + _ => (), + }); + + new_tier_thresholds + } else { + self.tier_thresholds.clone() + }; + + Self { + number_of_slots: new_number_of_slots, + slots_per_tier: new_slots_per_tier, + reward_portion: params.reward_portion.clone(), + tier_thresholds: new_tier_thresholds, + } + } + + /// Calculate number of slots, based on the provided native token price. + pub fn calculate_number_of_slots(native_price: FixedU64) -> u16 { + // floor(1000 x price + 50) + let result: u64 = native_price.saturating_mul_int(1000).saturating_add(50); + + result.unique_saturated_into() + } +} From 3d2d146f9f75cd863ed5abd134f95decd2670363 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 20 Oct 2023 17:13:22 +0200 Subject: [PATCH 15/86] Tier reward calculation WIP --- pallets/dapp-staking-v3/src/lib.rs | 93 +++++++++++++++++-- pallets/dapp-staking-v3/src/test/mock.rs | 39 +++++++- .../dapp-staking-v3/src/test/testing_utils.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 22 ++++- pallets/dapp-staking-v3/src/types.rs | 60 ++++++------ 5 files changed, 172 insertions(+), 46 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 348a101071..5973042aa7 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -45,6 +45,7 @@ use frame_support::{ weights::Weight, }; use frame_system::pallet_prelude::*; +use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ traits::{BadOrigin, Saturating, Zero}, Perbill, @@ -135,6 +136,10 @@ pub mod pallet { /// Minimum amount staker can stake on a contract. #[pallet::constant] type MinimumStakeAmount: Get; + + /// Number of different tiers. + #[pallet::constant] + type NumberOfTiers: Get; } #[pallet::event] @@ -285,6 +290,7 @@ pub mod pallet { #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; + // TODO: where to track TierLabels? E.g. a label to bootstrap a dApp into a specific tier. /// Map of all dApps integrated into dApp staking protocol. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< @@ -339,6 +345,21 @@ pub mod pallet { pub type PeriodEnd = StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; + /// Static tier parameters used to calculate tier configuration. + #[pallet::storage] + pub type StaticTierParams = + StorageValue<_, TierParameters, ValueQuery>; + + /// Tier configuration to be used during the newly started period + #[pallet::storage] + pub type NextTierConfig = + StorageValue<_, TierConfiguration, ValueQuery>; + + /// Tier configuration user for current & preceding eras. + #[pallet::storage] + pub type TierConfig = + StorageValue<_, TierConfiguration, ValueQuery>; + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(now: BlockNumberFor) -> Weight { @@ -363,8 +384,11 @@ pub mod pallet { let (maybe_period_event, era_reward) = match protocol_state.period_type() { PeriodType::Voting => { // For the sake of consistency, we put zero reward into storage - let era_reward = - EraReward::new(Balance::zero(), era_info.total_staked_amount()); + let era_reward = EraReward { + staker_reward_pool: Balance::zero(), + staked: era_info.total_staked_amount(), + dapp_reward_pool: Balance::zero(), + }; let ending_era = next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); @@ -374,6 +398,10 @@ pub mod pallet { era_info.migrate_to_next_era(Some(protocol_state.period_type())); + // Update tier configuration to be used when calculating rewards for the upcoming eras + let next_tier_config = NextTierConfig::::take(); + TierConfig::::put(next_tier_config); + ( Some(Event::::NewPeriod { period_type: protocol_state.period_type(), @@ -383,11 +411,15 @@ pub mod pallet { ) } PeriodType::BuildAndEarn => { - // TODO: trigger dAPp tier reward calculation here. This will be implemented later. + // 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 era_reward = - EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + let dapp_reward_pool = Balance::from(1_000_000_000u128); // TODO: same as above + let era_reward = EraReward { + staker_reward_pool, + staked: era_info.total_staked_amount(), + dapp_reward_pool, + }; // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { @@ -412,7 +444,12 @@ pub mod pallet { era_info.migrate_to_next_era(Some(protocol_state.period_type())); - // TODO: trigger tier configuration calculation based on internal & external params. + // Re-calculate tier configuration for the upcoming new period + let tier_params = StaticTierParams::::get(); + let average_price = FixedU64::from_rational(1, 10); // TODO: implement price fetching later + let new_tier_config = + TierConfig::::get().calculate_new(average_price, &tier_params); + NextTierConfig::::put(new_tier_config); ( Some(Event::::NewPeriod { @@ -1083,11 +1120,11 @@ pub mod pallet { .ok_or(Error::::InternalClaimStakerError)?; // Optimization, and zero-division protection - if amount.is_zero() || era_reward.staked().is_zero() { + if amount.is_zero() || era_reward.staked.is_zero() { continue; } - let staker_reward = Perbill::from_rational(amount, era_reward.staked()) - * era_reward.staker_reward_pool(); + let staker_reward = Perbill::from_rational(amount, era_reward.staked) + * era_reward.staker_reward_pool; rewards.push((era, staker_reward)); reward_sum.saturating_accrue(staker_reward); @@ -1234,7 +1271,7 @@ pub mod pallet { } /// `true` if smart contract is active, `false` if it has been unregistered. - fn is_active(smart_contract: &T::SmartContract) -> bool { + pub fn is_active(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) } @@ -1243,5 +1280,41 @@ pub mod pallet { pub fn era_reward_span_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } + + // 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) { + 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); + + // 1. + // This is bounded by max amount of dApps we allow to be registered + for (smart_contract, dapp_info) in IntegratedDApps::::iter() { + // Skip unregistered dApps + if dapp_info.state != DAppState::Registered { + continue; + } + + // Skip dApps which don't have ANY amount staked (TODO: potential improvement is to prune all dApps below minimum threshold) + let stake_amount = match ContractStake::::get(&smart_contract).get(era, period) { + Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, + _ => continue, + }; + + // TODO: maybe also push the 'Label' here? + dapp_stakes.push((dapp_info.id, stake_amount.total())); + } + + // 2. + // 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 + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 35196ce042..79c6b5f69f 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -28,6 +28,7 @@ use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, + Permill, }; use sp_io::TestExternalities; @@ -119,6 +120,7 @@ impl pallet_dapp_staking::Config for Test { type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU64<20>; type MinimumStakeAmount = ConstU128<3>; + type NumberOfTiers = ConstU32<4>; } // TODO: why not just change this to e.g. u32 for test? @@ -174,8 +176,43 @@ impl ExtBuilder { maintenance: false, }); + // TODO: improve this laterm should be handled via genesis? + let tier_params = TierParameters::<::NumberOfTiers> { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { amount: 1000 }, + TierThreshold::DynamicTvlAmount { amount: 500 }, + TierThreshold::DynamicTvlAmount { amount: 100 }, + TierThreshold::FixedTvlAmount { amount: 50 }, + ]) + .unwrap(), + }; + let init_tier_config = TierConfiguration::<::NumberOfTiers> { + number_of_slots: 100, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + + pallet_dapp_staking::StaticTierParams::::put(tier_params); + pallet_dapp_staking::TierConfig::::put(init_tier_config); + // DappStaking::on_initialize(System::block_number()); - }); + } + ); ext } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 644a71f6e9..4483b483f5 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -781,8 +781,8 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { .get(era) .expect("Entry must exist, otherwise 'claim' is invalid."); - let reward = Perbill::from_rational(amount, era_reward_info.staked()) - * era_reward_info.staker_reward_pool(); + let reward = Perbill::from_rational(amount, era_reward_info.staked) + * era_reward_info.staker_reward_pool; if reward.is_zero() { continue; } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 0eb4544dc1..48fb3df715 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1469,7 +1469,11 @@ fn era_reward_span_push_and_get_works() { // Insert some values and verify state change let era_1 = 5; - let era_reward_1 = EraReward::new(23, 41); + let era_reward_1 = EraReward { + staker_reward_pool: 23, + staked: 41, + dapp_reward_pool: 17, + }; assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); assert_eq!(era_reward_span.len(), 1); assert_eq!(era_reward_span.first_era(), era_1); @@ -1477,7 +1481,11 @@ fn era_reward_span_push_and_get_works() { // Insert another value and verify state change let era_2 = era_1 + 1; - let era_reward_2 = EraReward::new(37, 53); + let era_reward_2 = EraReward { + staker_reward_pool: 37, + staked: 53, + dapp_reward_pool: 19, + }; assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); assert_eq!(era_reward_span.len(), 2); assert_eq!(era_reward_span.first_era(), era_1); @@ -1496,7 +1504,11 @@ fn era_reward_span_fails_when_expected() { // Push first values to get started let era_1 = 5; - let era_reward = EraReward::new(23, 41); + let era_reward = EraReward { + staker_reward_pool: 23, + staked: 41, + dapp_reward_pool: 17, + }; assert!(era_reward_span.push(era_1, era_reward).is_ok()); // Attempting to push incorrect era results in an error @@ -1521,7 +1533,7 @@ fn era_reward_span_fails_when_expected() { fn tier_slot_configuration_basic_tests() { // TODO: this should be expanded & improved later get_u32_type!(TiersNum, 4); - let params = TierSlotParameters:: { + let params = TierParameters:: { reward_portion: BoundedVec::try_from(vec![ Permill::from_percent(40), Permill::from_percent(30), @@ -1547,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 = TierSlotConfiguration:: { + let init_config = TierConfiguration:: { 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 20a98e41e8..912019ce2a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -288,7 +288,7 @@ where fn default() -> Self { Self { locked: Balance::zero(), - unlocking: BoundedVec::, UnlockingLen>::default(), + unlocking: BoundedVec::default(), staked: StakeAmount::default(), staked_future: None, staker_rewards_claimed: false, @@ -1239,30 +1239,13 @@ impl ContractStakeAmountSeries { pub struct EraReward { /// Total reward pool for staker rewards #[codec(compact)] - staker_reward_pool: Balance, + pub staker_reward_pool: Balance, /// Total amount which was staked at the end of an era #[codec(compact)] - staked: Balance, -} - -impl EraReward { - /// Create new instance of `EraReward` with specified `staker_reward_pool` and `staked` amounts. - pub fn new(staker_reward_pool: Balance, staked: Balance) -> Self { - Self { - staker_reward_pool, - staked, - } - } - - /// Total reward pool for staker rewards. - pub fn staker_reward_pool(&self) -> Balance { - self.staker_reward_pool - } - - /// Total amount which was staked at the end of an era. - pub fn staked(&self) -> Balance { - self.staked - } + pub staked: Balance, + /// Total reward pool for dApp rewards + #[codec(compact)] + pub dapp_reward_pool: Balance, } #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] @@ -1375,7 +1358,7 @@ pub enum TierThreshold { /// 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))] -pub struct TierSlotParameters> { +pub struct TierParameters> { /// Reward distribution per tier, in percentage. /// First entry refers to the first tier, and so on. /// The sum of all values must be exactly equal to 1. @@ -1389,7 +1372,7 @@ pub struct TierSlotParameters> { pub tier_thresholds: BoundedVec, } -impl> TierSlotParameters { +impl> TierParameters { /// Check if configuration is valid. /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`. pub fn is_valid(&self) -> bool { @@ -1400,10 +1383,20 @@ impl> TierSlotParameters { } } +impl> Default for TierParameters { + fn default() -> Self { + Self { + reward_portion: BoundedVec::default(), + slot_distribution: BoundedVec::default(), + tier_thresholds: BoundedVec::default(), + } + } +} + /// Configuration of dApp tiers. #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(NT))] -pub struct TierSlotConfiguration> { +pub struct TierConfiguration> { /// Total number of slots. #[codec(compact)] pub number_of_slots: u16, @@ -1419,7 +1412,18 @@ pub struct TierSlotConfiguration> { pub tier_thresholds: BoundedVec, } -impl> TierSlotConfiguration { +impl> Default for TierConfiguration { + fn default() -> Self { + Self { + number_of_slots: 0, + slots_per_tier: BoundedVec::default(), + reward_portion: BoundedVec::default(), + tier_thresholds: BoundedVec::default(), + } + } +} + +impl> TierConfiguration { /// Check if parameters are valid. pub fn is_valid(&self) -> bool { let number_of_tiers: usize = NT::get() as usize; @@ -1432,7 +1436,7 @@ impl> TierSlotConfiguration { } /// TODO - pub fn calculate_new(&self, native_price: FixedU64, params: &TierSlotParameters) -> Self { + pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { let new_number_of_slots = Self::calculate_number_of_slots(native_price); // TODO: ugly, unsafe, refactor later From 894d10d5c5e35fd72c357b224333f04fee259330 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 23 Oct 2023 16:20:31 +0200 Subject: [PATCH 16/86] 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(), + } + } +} From ff61cf10b14b9fe957a21376861e189af3dd2e88 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 24 Oct 2023 10:46:24 +0200 Subject: [PATCH 17/86] Minor cleanup --- pallets/dapp-staking-v3/src/lib.rs | 10 ---------- pallets/dapp-staking-v3/src/test/mock.rs | 9 ++++----- pallets/dapp-staking-v3/src/types.rs | 2 +- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 53583f956d..663abbf612 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1060,7 +1060,6 @@ pub mod pallet { Ok(()) } - // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. /// TODO: docs #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] @@ -1125,7 +1124,6 @@ pub mod pallet { let mut rewards: Vec<_> = Vec::new(); let mut reward_sum = Balance::zero(); for (era, amount) in rewards_iter { - // TODO: this should be zipped, and values should be fetched only once let era_reward = era_rewards .get(era) .ok_or(Error::::InternalClaimStakerError)?; @@ -1208,14 +1206,6 @@ pub mod pallet { Error::::InternalClaimBonusError ); - // TODO: this functionality is incomplete - what we should do is iterate over all stake entries in the storage, - // and check if the smart contract was still registered when period ended. - // - // This is important since we cannot allow unregistered contracts to be subject for bonus rewards. - // This means 'a loop' but it will be bounded by max limit of unique stakes. - // A function should also be introduced to prepare the account ledger for next era (or to cleanup old expired rewards) - // in case bonus rewards weren't claimed. - let bonus_reward = Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 79c6b5f69f..2f498d2efc 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -123,7 +123,6 @@ impl pallet_dapp_staking::Config for Test { type NumberOfTiers = ConstU32<4>; } -// TODO: why not just change this to e.g. u32 for test? #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] pub enum MockSmartContract { Wasm(AccountId), @@ -193,14 +192,14 @@ impl ExtBuilder { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { amount: 1000 }, - TierThreshold::DynamicTvlAmount { amount: 500 }, TierThreshold::DynamicTvlAmount { amount: 100 }, - TierThreshold::FixedTvlAmount { amount: 50 }, + TierThreshold::DynamicTvlAmount { amount: 50 }, + TierThreshold::DynamicTvlAmount { amount: 20 }, + TierThreshold::FixedTvlAmount { amount: 10 }, ]) .unwrap(), }; - let init_tier_config = TierConfiguration::<::NumberOfTiers> { + let init_tier_config = TiersConfiguration::<::NumberOfTiers> { number_of_slots: 100, slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), reward_portion: tier_params.reward_portion.clone(), diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index d84f91b94f..9df7a537da 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -1415,7 +1415,7 @@ 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 +// 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)] From 3ded04680bc5084e5c318223eff65ccb24eecb32 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 24 Oct 2023 11:32:05 +0200 Subject: [PATCH 18/86] Claim dapp rewards --- pallets/dapp-staking-v3/src/lib.rs | 68 +++++++++++++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 49 +++++++++++++++----- 2 files changed, 104 insertions(+), 13 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 663abbf612..7eb49627ba 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -216,6 +216,13 @@ pub mod pallet { period: PeriodNumber, amount: Balance, }, + DAppReward { + beneficiary: T::AccountId, + smart_contract: T::SmartContract, + tier_id: TierId, + era: EraNumber, + amount: Balance, + }, } #[pallet::error] @@ -269,7 +276,7 @@ pub mod pallet { RewardExpired, /// All staker rewards for the period have been claimed. StakerRewardsAlreadyClaimed, - /// There are no claimable rewards for the account. + /// There are no claimable rewards. NoClaimableRewards, /// An unexpected error occured while trying to claim staker rewards. InternalClaimStakerError, @@ -279,6 +286,15 @@ pub mod pallet { NotEligibleForBonusReward, /// An unexpected error occured while trying to claim bonus reward. InternalClaimBonusError, + /// Claim era is invalid - it must be in history, and rewards must exist for it. + InvalidClaimEra, + /// No dApp tier info exists for the specified era. This can be because era has expired + /// or because during the specified era there were no eligible rewards or protocol wasn't active. + NoDAppTierInfo, + /// dApp reward has already been claimed for this era. + DAppRewardAlreadyClaimed, + /// An unexpected error occured while trying to claim dApp reward. + InternalClaimDAppError, } /// General information about dApp staking protocol state. @@ -1139,6 +1155,7 @@ pub mod pallet { reward_sum.saturating_accrue(staker_reward); } + // Account exists since it has locked funds. T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; @@ -1210,6 +1227,7 @@ pub mod pallet { Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; + // Account exists since it has locked funds. T::Currency::deposit_into_existing(&account, bonus_reward) .map_err(|_| Error::::InternalClaimStakerError)?; @@ -1223,6 +1241,54 @@ pub mod pallet { Ok(()) } + + /// TODO: documentation + #[pallet::call_index(13)] + #[pallet::weight(Weight::zero())] + pub fn claim_dapp_reward( + origin: OriginFor, + smart_contract: T::SmartContract, + #[pallet::compact] era: EraNumber, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let _ = ensure_signed(origin)?; + + let dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::ContractNotFound)?; + + // Make sure provided era has ended + let protocol_state = ActiveProtocolState::::get(); + ensure!(era < protocol_state.era, Error::::InvalidClaimEra); + + // 'Consume' dApp reward for the specified era, if possible. + let mut dapp_tiers = DAppTiers::::get(&era).ok_or(Error::::NoDAppTierInfo)?; + let (amount, tier_id) = + dapp_tiers + .try_consume(dapp_info.id) + .map_err(|error| match error { + DAppTierError::NoDAppInTiers => Error::::NoClaimableRewards, + DAppTierError::RewardAlreadyClaimed => Error::::DAppRewardAlreadyClaimed, + _ => Error::::InternalClaimDAppError, + })?; + + // Get reward destination, and deposit the reward. + // TODO: should we check reward is greater than zero, or even more precise, it's greater than the existential deposit? Seems redundant but still... + let reward_destination = dapp_info.get_reward_destination(); + T::Currency::deposit_creating(reward_destination, amount); + + // Write back updated struct to prevent double reward claims + DAppTiers::::insert(&era, dapp_tiers); + + Self::deposit_event(Event::::DAppReward { + beneficiary: reward_destination.clone(), + smart_contract, + tier_id, + era, + amount, + }); + + Ok(()) + } } impl Pallet { diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 9df7a537da..ef58dac0ba 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -52,6 +52,8 @@ pub type EraNumber = u32; pub type PeriodNumber = u32; /// Dapp Id type pub type DAppId = u16; +/// Tier Id type +pub type TierId = u8; /// Simple enum representing errors possible when using sparse bounded vector. #[derive(Debug, PartialEq, Eq)] @@ -243,6 +245,16 @@ pub struct DAppInfo { pub reward_destination: Option, } +impl DAppInfo { + /// Reward destination account for this dApp. + pub fn get_reward_destination(&self) -> &AccountId { + match &self.reward_destination { + Some(account_id) => account_id, + None => &self.owner, + } + } +} + /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct UnlockingChunk { @@ -1542,7 +1554,7 @@ pub struct DAppTier { #[codec(compact)] pub dapp_id: DAppId, /// `Some(tier_id)` if dApp belongs to tier and has unclaimed rewards, `None` otherwise. - pub tier_id: Option, + pub tier_id: Option, } /// Information about all of the dApps that got into tiers, and tier rewards @@ -1573,32 +1585,45 @@ impl, NT: Get> DAppTierRewards { 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 { + /// Consume reward for the specified dapp id, returning its amount and tier Id. + /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`. + pub fn try_consume(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { // 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(), + _ => { + return Err(DAppTierError::NoDAppInTiers); + } }; 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) + Ok(( + self.rewards + .get(tier_id as usize) + .map_or(Balance::zero(), |x| *x), + tier_id, + )) } else { - // In case reward has already been claimed - Balance::zero() + Err(DAppTierError::RewardAlreadyClaimed) } } // unreachable code, at this point it was proved that index exists - _ => Balance::zero(), + _ => Err(DAppTierError::InternalError), } } } + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DAppTierError { + /// Specified dApp Id doesn't exist in any tier. + NoDAppInTiers, + /// Reward has already been claimed for this dApp. + RewardAlreadyClaimed, + /// Internal, unexpected error occured. + InternalError, +} From f2f8a5f95b7d9fa23fcdae65eb039650d16f50cc Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 24 Oct 2023 13:59:13 +0200 Subject: [PATCH 19/86] Claim dapp reward tests --- pallets/dapp-staking-v3/src/lib.rs | 10 +- pallets/dapp-staking-v3/src/test/mock.rs | 3 +- .../dapp-staking-v3/src/test/testing_utils.rs | 70 ++++++++- pallets/dapp-staking-v3/src/test/tests.rs | 141 ++++++++++++++++++ pallets/dapp-staking-v3/src/types.rs | 4 +- 5 files changed, 221 insertions(+), 7 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 7eb49627ba..5725fe30c2 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1273,14 +1273,14 @@ pub mod pallet { // Get reward destination, and deposit the reward. // TODO: should we check reward is greater than zero, or even more precise, it's greater than the existential deposit? Seems redundant but still... - let reward_destination = dapp_info.get_reward_destination(); - T::Currency::deposit_creating(reward_destination, amount); + let beneficiary = dapp_info.get_reward_beneficiary(); + T::Currency::deposit_creating(beneficiary, amount); // Write back updated struct to prevent double reward claims DAppTiers::::insert(&era, dapp_tiers); Self::deposit_event(Event::::DAppReward { - beneficiary: reward_destination.clone(), + beneficiary: beneficiary.clone(), smart_contract, tier_id, era, @@ -1397,7 +1397,9 @@ pub mod pallet { .iter() .zip(tier_config.tier_thresholds.iter()) { - let max_idx = global_idx.saturating_add(*tier_capacity as usize); + 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 diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 2f498d2efc..18471fa9e0 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -207,7 +207,8 @@ impl ExtBuilder { }; pallet_dapp_staking::StaticTierParams::::put(tier_params); - pallet_dapp_staking::TierConfig::::put(init_tier_config); + pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); + pallet_dapp_staking::NextTierConfig::::put(init_tier_config); // DappStaking::on_initialize(System::block_number()); } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 4483b483f5..2e171d1616 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -20,7 +20,8 @@ use crate::test::mock::*; use crate::types::*; use crate::{ pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, - EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, PeriodEndInfo, StakerInfo, + DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, PeriodEndInfo, + StakerInfo, }; use frame_support::{assert_ok, traits::Get}; @@ -49,6 +50,7 @@ pub(crate) struct MemorySnapshot { contract_stake: HashMap<::SmartContract, ContractStakeAmountSeries>, era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, + dapp_tiers: HashMap>, } impl MemorySnapshot { @@ -66,6 +68,7 @@ impl MemorySnapshot { contract_stake: ContractStake::::iter().collect(), era_rewards: EraRewards::::iter().collect(), period_end: PeriodEnd::::iter().collect(), + dapp_tiers: DAppTiers::::iter().collect(), } } @@ -907,6 +910,71 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId) { } } +/// Claim dapp reward for a particular era. +pub(crate) fn assert_claim_dapp_reward( + account: AccountId, + smart_contract: &MockSmartContract, + era: EraNumber, +) { + let pre_snapshot = MemorySnapshot::new(); + let dapp_info = pre_snapshot.integrated_dapps.get(smart_contract).unwrap(); + let beneficiary = dapp_info.get_reward_beneficiary(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(beneficiary); + + let (expected_reward, expected_tier_id) = { + let mut info = pre_snapshot + .dapp_tiers + .get(&era) + .expect("Entry must exist.") + .clone(); + + info.try_consume(dapp_info.id).unwrap() + }; + + // Claim dApp reward & verify event + assert_ok!(DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract.clone(), + era, + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::DAppReward { + beneficiary: beneficiary.clone(), + smart_contract: smart_contract.clone(), + tier_id: expected_tier_id, + era, + amount: expected_reward, + })); + + // Verify post-state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + expected_reward, + "Total issuance must increase by the reward amount." + ); + + let post_free_balance = ::Currency::free_balance(beneficiary); + assert_eq!( + post_free_balance, + pre_free_balance + expected_reward, + "Free balance must increase by the reward amount." + ); + + let post_snapshot = MemorySnapshot::new(); + let mut info = post_snapshot + .dapp_tiers + .get(&era) + .expect("Entry must exist.") + .clone(); + assert_eq!( + info.try_consume(dapp_info.id), + Err(DAppTierError::RewardAlreadyClaimed), + "It must not be possible to claim the same reward twice!.", + ); +} + /// Returns from which starting era to which ending era can rewards be claimed for the specified account. /// /// If `None` is returned, there is nothing to claim. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 87716c6c5f..f0914fa0e2 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1406,3 +1406,144 @@ fn claim_bonus_reward_after_expiry_fails() { ); }) } + +#[test] +fn claim_dapp_reward_works() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 2 eras so we have an entry for reward claiming + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_eq!(ActiveProtocolState::::get().era, 3, "Sanity check"); + + assert_claim_dapp_reward( + account, + &smart_contract, + ActiveProtocolState::::get().era - 1, + ); + + // Advance to next era, and ensure rewards can be paid out to a custom beneficiary + let new_beneficiary = 17; + assert_set_dapp_reward_destination(dev_account, &smart_contract, Some(new_beneficiary)); + advance_to_next_era(); + assert_claim_dapp_reward( + account, + &smart_contract, + ActiveProtocolState::::get().era - 1, + ); + }) +} + +#[test] +fn claim_dapp_reward_from_non_existing_contract_fails() { + ExtBuilder::build().execute_with(|| { + let smart_contract = MockSmartContract::default(); + assert_noop!( + DappStaking::claim_dapp_reward(RuntimeOrigin::signed(1), smart_contract, 1), + Error::::ContractNotFound, + ); + }) +} + +#[test] +fn claim_dapp_reward_from_invalid_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 2 eras and try to claim from the ongoing era. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(1), + smart_contract, + ActiveProtocolState::::get().era + ), + Error::::InvalidClaimEra, + ); + + // Try to claim from the era which corresponds to the voting period. No tier info should + assert_noop!( + DappStaking::claim_dapp_reward(RuntimeOrigin::signed(1), smart_contract, 1), + Error::::NoDAppTierInfo, + ); + }) +} + +#[test] +fn claim_dapp_reward_if_dapp_not_in_any_tier_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract_1 = MockSmartContract::Wasm(3); + let smart_contract_2 = MockSmartContract::Wasm(5); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract_1, amount); + + // Advance 2 eras and try to claim reward for non-staked dApp. + advance_to_era(ActiveProtocolState::::get().era + 2); + let account = 2; + let claim_era = ActiveProtocolState::::get().era - 1; + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract_2, + claim_era + ), + Error::::NoClaimableRewards, + ); + // Staked dApp should still be able to claim. + assert_claim_dapp_reward(account, &smart_contract_1, claim_era); + }) +} + +#[test] +fn claim_dapp_reward_twice_for_same_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 3 eras and claim rewards. + advance_to_era(ActiveProtocolState::::get().era + 3); + + // We can only claim reward ONCE for a particular era + let claim_era_1 = ActiveProtocolState::::get().era - 2; + assert_claim_dapp_reward(account, &smart_contract, claim_era_1); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract, + claim_era_1 + ), + Error::::DAppRewardAlreadyClaimed, + ); + + // We can still claim for another valid era + let claim_era_2 = claim_era_1 + 1; + assert_claim_dapp_reward(account, &smart_contract, claim_era_2); + }) +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index ef58dac0ba..b88dffc64f 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -39,6 +39,7 @@ pub type DAppTierRewardsFor = DAppTierRewards, ::NumberOfTiers>; // Helper struct for converting `u16` getter into `u32` +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct MaxNumberOfContractsU32(PhantomData); impl Get for MaxNumberOfContractsU32 { fn get() -> u32 { @@ -247,7 +248,7 @@ pub struct DAppInfo { impl DAppInfo { /// Reward destination account for this dApp. - pub fn get_reward_destination(&self) -> &AccountId { + pub fn get_reward_beneficiary(&self) -> &AccountId { match &self.reward_destination { Some(account_id) => account_id, None => &self.owner, @@ -1565,6 +1566,7 @@ pub struct DAppTierRewards, NT: Get> { pub dapps: BoundedVec, /// Rewards for each tier. First entry refers to the first tier, and so on. pub rewards: BoundedVec, + // TODO: perhaps I can add 'PeriodNumber' here so it's easy to identify expired rewards } impl, NT: Get> Default for DAppTierRewards { From 03d88f40c0d7129c5b4eadbd10ea0f009c4d75db Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 24 Oct 2023 14:51:28 +0200 Subject: [PATCH 20/86] unstake from unregistered call --- pallets/dapp-staking-v3/src/lib.rs | 79 ++++++++++++++++++- .../dapp-staking-v3/src/test/testing_utils.rs | 1 + pallets/dapp-staking-v3/src/test/tests.rs | 18 ++++- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 5725fe30c2..8829f97210 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -223,6 +223,11 @@ pub mod pallet { era: EraNumber, amount: Balance, }, + UnstakeFromUnregistered { + account: T::AccountId, + smart_contract: T::SmartContract, + amount: Balance, + }, } #[pallet::error] @@ -295,6 +300,8 @@ pub mod pallet { DAppRewardAlreadyClaimed, /// An unexpected error occured while trying to claim dApp reward. InternalClaimDAppError, + /// Contract is still active, not unregistered. + ContractStillActive, } /// General information about dApp staking protocol state. @@ -693,9 +700,10 @@ pub mod pallet { }, )?; - // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. - - // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' + // As a consequence, this means that the sum of all stakes from `ContractStake` storage won't match + // total stake in active era info. This is fine, from the logic perspective, it doesn't cause any issues. + // TODO: left as potential discussion topic, remove later. + ContractStake::::remove(&smart_contract); // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' @@ -1289,6 +1297,71 @@ pub mod pallet { Ok(()) } + + /// TODO + #[pallet::call_index(14)] + #[pallet::weight(Weight::zero())] + pub fn unstake_from_unregistered( + origin: OriginFor, + smart_contract: T::SmartContract, + ) -> DispatchResult { + // TODO: this call needs to be tested + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + ensure!( + !Self::is_active(&smart_contract), + Error::::ContractStillActive + ); + + let protocol_state = ActiveProtocolState::::get(); + let unstake_era = protocol_state.era; + + // Extract total staked amount on the specified unregistered contract + let amount = match StakerInfo::::get(&account, &smart_contract) { + Some(staking_info) => { + ensure!( + staking_info.period_number() == protocol_state.period_number(), + Error::::UnstakeFromPastPeriod + ); + + staking_info.total_staked_amount() + } + None => { + return Err(Error::::NoStakingInfo.into()); + } + }; + + // Reduce stake amount in ledger + let mut ledger = Ledger::::get(&account); + ledger + .unstake_amount(amount, unstake_era, protocol_state.period_info) + .map_err(|err| match err { + // These are all defensive checks, which should never happen since we already checked them above. + AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { + Error::::UnclaimedRewardsFromPastPeriods + } + _ => Error::::InternalUnstakeError, + })?; + + // Update total staked amount for the next era. + // This means 'fake' stake total amount has been kept until now, even though contract was unregistered. + CurrentEraInfo::::mutate(|era_info| { + era_info.unstake_amount(amount, protocol_state.period_type()); + }); + + // Update remaining storage entries + Self::update_ledger(&account, ledger); + StakerInfo::::remove(&account, &smart_contract); + + Self::deposit_event(Event::::UnstakeFromUnregistered { + account, + smart_contract, + amount, + }); + + Ok(()) + } } impl Pallet { diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 2e171d1616..9e8d9318ea 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -187,6 +187,7 @@ pub(crate) fn assert_unregister(smart_contract: &MockSmartContract) { IntegratedDApps::::get(&smart_contract).unwrap().state, DAppState::Unregistered(pre_snapshot.active_protocol_state.era), ); + assert!(!ContractStake::::contains_key(&smart_contract)); } /// Lock funds into dApp staking and assert success. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index f0914fa0e2..3cdcbfa2d5 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -330,13 +330,29 @@ fn set_dapp_owner_fails() { } #[test] -fn unregister_is_ok() { +fn unregister_no_stake_is_ok() { ExtBuilder::build().execute_with(|| { // Prepare dApp let owner = 1; let smart_contract = MockSmartContract::Wasm(3); assert_register(owner, &smart_contract); + // Nothing staked on contract, just unregister it. + assert_unregister(&smart_contract); + }) +} + +#[test] +fn unregister_with_active_stake_is_ok() { + ExtBuilder::build().execute_with(|| { + // Prepare dApp + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + assert_register(owner, &smart_contract); + assert_lock(owner, 100); + assert_stake(owner, &smart_contract, 100); + + // Some amount is staked, unregister must still work. assert_unregister(&smart_contract); }) } From ea3e23d6b6ddfd79be57cebd32f4bed1772b39c0 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 24 Oct 2023 16:38:58 +0200 Subject: [PATCH 21/86] Extra traits --- pallets/dapp-staking-v3/src/lib.rs | 64 ++++++++------- pallets/dapp-staking-v3/src/test/mock.rs | 54 +++++++++---- .../dapp-staking-v3/src/test/tests_types.rs | 15 +++- pallets/dapp-staking-v3/src/types.rs | 81 ++++++++++++++++--- 4 files changed, 158 insertions(+), 56 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8829f97210..afffe8ca94 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -17,6 +17,7 @@ // along with Astar. If not, see . //! # dApp Staking v3 Pallet +//! TODO //! //! - [`Config`] //! @@ -93,6 +94,12 @@ pub mod pallet { /// Privileged origin for managing dApp staking pallet. type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; + /// Used to provide price information about the native token. + type NativePriceProvider: PriceProvider; + + /// Used to provide reward pools amount. + type RewardPoolProvider: RewardPoolProvider; + /// Length of a standard era in block numbers. #[pallet::constant] type StandardEraLength: Get; @@ -439,8 +446,8 @@ pub mod pallet { ) } PeriodType::BuildAndEarn => { - 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 (staker_reward_pool, dapp_reward_pool) = + T::RewardPoolProvider::normal_reward_pools(); let era_reward = EraReward { staker_reward_pool, staked: era_info.total_staked_amount(), @@ -458,7 +465,7 @@ pub mod pallet { // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { // Store info about period end - let bonus_reward_pool = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet + let bonus_reward_pool = T::RewardPoolProvider::bonus_reward_pool(); PeriodEnd::::insert( &protocol_state.period_number(), PeriodEndInfo { @@ -480,7 +487,7 @@ pub mod pallet { // Re-calculate tier configuration for the upcoming new period let tier_params = StaticTierParams::::get(); - let average_price = FixedU64::from_rational(1, 10); // TODO: implement price fetching later + let average_price = T::NativePriceProvider::average_price(); let new_tier_config = TierConfig::::get().calculate_new(average_price, &tier_params); NextTierConfig::::put(new_tier_config); @@ -512,9 +519,7 @@ pub mod pallet { let era_span_index = Self::era_reward_span_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); - // TODO: error must not happen here. Log an error if it does. - // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. - // Will require a fix from the runtime team though. + // TODO: Error "cannot" happen here. Log an error if it does though. let _ = span.push(current_era, era_reward); EraRewards::::insert(&era_span_index, span); @@ -705,8 +710,6 @@ pub mod pallet { // TODO: left as potential discussion topic, remove later. ContractStake::::remove(&smart_contract); - // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' - Self::deposit_event(Event::::DAppUnregistered { smart_contract, era: current_era, @@ -1084,7 +1087,9 @@ pub mod pallet { Ok(()) } - /// TODO: docs + /// Claims some staker rewards, if user has any. + /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening + /// if appropriate entries exist in account's ledger. #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { @@ -1181,7 +1186,7 @@ pub mod pallet { } // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. - /// TODO: documentation + /// Used to claim bonus reward for the eligible era, if applicable. #[pallet::call_index(12)] #[pallet::weight(Weight::zero())] pub fn claim_bonus_reward(origin: OriginFor) -> DispatchResult { @@ -1250,7 +1255,7 @@ pub mod pallet { Ok(()) } - /// TODO: documentation + /// Used to claim dApp reward for the specified era. #[pallet::call_index(13)] #[pallet::weight(Weight::zero())] pub fn claim_dapp_reward( @@ -1298,14 +1303,14 @@ pub mod pallet { Ok(()) } - /// TODO + /// Used to unstake funds from a contract that was unregistered after an account staked on it. #[pallet::call_index(14)] #[pallet::weight(Weight::zero())] pub fn unstake_from_unregistered( origin: OriginFor, smart_contract: T::SmartContract, ) -> DispatchResult { - // TODO: this call needs to be tested + // TODO: tests are missing but will be added later. Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1421,19 +1426,24 @@ pub mod pallet { era.saturating_sub(era % T::EraRewardSpanLength::get()) } - // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. - // TODO2: documentation + /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. + /// + /// The returned object contains information about each dApp that made it into a tier. pub fn get_dapp_tier_assignment( era: EraNumber, period: PeriodNumber, dapp_reward_pool: Balance, ) -> DAppTierRewardsFor { - let tier_config = TierConfig::::get(); + // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. + // Benchmarks will show this, but I don't believe it will be needed, especially with increased block capacity we'll get with async backing. + // Even without async backing though, we should have enough capacity to handle this. + let tier_config = TierConfig::::get(); let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); // 1. - // This is bounded by max amount of dApps we allow to be registered + // Iterate over all registered dApps, and collect their stake amount. + // This is bounded by max amount of dApps we allow to be registered. for (smart_contract, dapp_info) in IntegratedDApps::::iter() { // Skip unregistered dApps if dapp_info.state != DAppState::Registered { @@ -1446,8 +1456,8 @@ pub mod pallet { _ => continue, }; - // TODO: maybe also push the 'Label' here? - // TODO2: proposition for label handling: + // TODO: Need to handle labels! + // 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. @@ -1460,7 +1470,9 @@ pub mod pallet { dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1)); // 3. - // Calculate slices representing each tier + // 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 = Vec::with_capacity(dapp_stakes.len()); let mut global_idx = 0; @@ -1476,7 +1488,7 @@ pub mod pallet { // 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) + // 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_accrue(1); @@ -1493,11 +1505,7 @@ pub mod pallet { } // 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. + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids). dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); // 5. Calculate rewards. @@ -1508,7 +1516,7 @@ pub mod pallet { .collect::>(); // 6. - // Prepare and return tier & rewards info + // 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, diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 18471fa9e0..a920b6a3dc 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -24,6 +24,7 @@ use frame_support::{ weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -105,11 +106,45 @@ impl pallet_balances::Config for Test { type WeightInfo = (); } +pub struct DummyPriceProvider; +impl PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyRewardPoolProvider; +impl RewardPoolProvider for DummyRewardPoolProvider { + fn normal_reward_pools() -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000_u128), + Balance::from(1_000_000_000_u128), + ) + } + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000_u128) + } +} + +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] +pub enum MockSmartContract { + Wasm(AccountId), + Other(AccountId), +} + +impl Default for MockSmartContract { + fn default() -> Self { + MockSmartContract::Wasm(1) + } +} + impl pallet_dapp_staking::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type SmartContract = MockSmartContract; type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type RewardPoolProvider = DummyRewardPoolProvider; type StandardEraLength = ConstU64<10>; type StandardErasPerVotingPeriod = ConstU32<8>; type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; @@ -123,18 +158,6 @@ impl pallet_dapp_staking::Config for Test { type NumberOfTiers = ConstU32<4>; } -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -pub enum MockSmartContract { - Wasm(AccountId), - Other(AccountId), -} - -impl Default for MockSmartContract { - fn default() -> Self { - MockSmartContract::Wasm(1) - } -} - pub struct ExtBuilder; impl ExtBuilder { pub fn build() -> TestExternalities { @@ -192,9 +215,9 @@ impl ExtBuilder { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { amount: 100 }, - TierThreshold::DynamicTvlAmount { amount: 50 }, - TierThreshold::DynamicTvlAmount { amount: 20 }, + TierThreshold::DynamicTvlAmount { amount: 100, minimum_amount: 80 }, + TierThreshold::DynamicTvlAmount { amount: 50, minimum_amount: 40 }, + TierThreshold::DynamicTvlAmount { amount: 20, minimum_amount: 20 }, TierThreshold::FixedTvlAmount { amount: 10 }, ]) .unwrap(), @@ -210,6 +233,7 @@ impl ExtBuilder { pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); pallet_dapp_staking::NextTierConfig::::put(init_tier_config); + // TODO: include this into every test unless it explicitly doesn't need ot/ // DappStaking::on_initialize(System::block_number()); } ); diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 24c92bf563..62cd148911 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1549,9 +1549,18 @@ fn tier_slot_configuration_basic_tests() { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { amount: 1000 }, - TierThreshold::DynamicTvlAmount { amount: 500 }, - TierThreshold::DynamicTvlAmount { amount: 100 }, + TierThreshold::DynamicTvlAmount { + amount: 1000, + minimum_amount: 800, + }, + TierThreshold::DynamicTvlAmount { + amount: 500, + minimum_amount: 350, + }, + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 70, + }, TierThreshold::FixedTvlAmount { amount: 50 }, ]) .unwrap(), diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index b88dffc64f..38b25fd045 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -1375,9 +1375,11 @@ pub enum TierThreshold { FixedTvlAmount { amount: Balance }, /// Entry into tier is mandated by minimum amount of staked funds. /// Value is expected to dynamically change in-between periods, depending on the system parameters. - DynamicTvlAmount { amount: Balance }, - // TODO: perhaps add a type that is dynamic but has lower bound below which the value cannot go? - // Otherwise we could allow e.g. tier 3 to go below tier 4, which doesn't make sense. + /// The `amount` should never go below the `minimum_amount`. + DynamicTvlAmount { + amount: Balance, + minimum_amount: Balance, + }, } impl TierThreshold { @@ -1385,7 +1387,7 @@ impl TierThreshold { pub fn is_satisfied(&self, stake: Balance) -> bool { match self { Self::FixedTvlAmount { amount } => stake >= *amount, - Self::DynamicTvlAmount { amount } => stake >= *amount, + Self::DynamicTvlAmount { amount, .. } => stake >= *amount, } } } @@ -1476,7 +1478,7 @@ impl> TiersConfiguration { pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { let new_number_of_slots = Self::calculate_number_of_slots(native_price); - // TODO: ugly, unsafe, refactor later + // Calculate how much each tier gets slots. let new_slots_per_tier: Vec = params .slot_distribution .clone() @@ -1488,7 +1490,31 @@ impl> TiersConfiguration { let new_slots_per_tier = BoundedVec::::try_from(new_slots_per_tier).unwrap_or_default(); - // TODO: document this! + // Update tier thresholds. + // In case number of slots increase, we decrease thresholds required to enter the tier. + // In case number of slots decrease, we increase the threshold required to enter the tier. + // + // According to formula: %_threshold = (100% / (100% - delta_%_slots) - 1) * 100% + // + // where delta_%_slots is simply: (old_num_slots - new_num_slots) / old_num_slots + // + // When these entries are put into the threshold formula, we get: + // = 1 / ( 1 - (old_num_slots - new_num_slots) / old_num_slots ) - 1 + // = 1 / ( new / old) - 1 + // = old / new - 1 + // = (old - new) / new + // + // This number can be negative. In order to keep all operations in unsigned integer domain, + // formulas are adjusted like: + // + // 1. Number of slots has increased, threshold is expected to decrease + // %_threshold = (new_num_slots - old_num_slots) / new_num_slots + // new_threshold = old_threshold * (1 - %_threshold) + // + // 2. Number of slots has decreased, threshold is expected to increase + // %_threshold = (old_num_slots - new_num_slots) / new_num_slots + // new_threshold = old_threshold * (1 + %_threshold) + // 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(), @@ -1499,9 +1525,13 @@ impl> TiersConfiguration { new_tier_thresholds .iter_mut() .for_each(|threshold| match threshold { - TierThreshold::DynamicTvlAmount { amount } => { + TierThreshold::DynamicTvlAmount { + amount, + minimum_amount, + } => { *amount = amount .saturating_sub(delta_threshold_decrease.saturating_mul_int(*amount)); + *amount = *amount.max(minimum_amount); } _ => (), }); @@ -1517,7 +1547,7 @@ impl> TiersConfiguration { new_tier_thresholds .iter_mut() .for_each(|threshold| match threshold { - TierThreshold::DynamicTvlAmount { amount } => { + TierThreshold::DynamicTvlAmount { amount, .. } => { *amount = amount .saturating_add(delta_threshold_increase.saturating_mul_int(*amount)); } @@ -1539,7 +1569,7 @@ impl> TiersConfiguration { /// Calculate number of slots, based on the provided native token price. pub fn calculate_number_of_slots(native_price: FixedU64) -> u16 { - // floor(1000 x price + 50) + // floor(1000 x price + 50), formula proposed in Tokenomics 2.0 document. let result: u64 = native_price.saturating_mul_int(1000).saturating_add(50); result.unique_saturated_into() @@ -1566,7 +1596,7 @@ pub struct DAppTierRewards, NT: Get> { pub dapps: BoundedVec, /// Rewards for each tier. First entry refers to the first tier, and so on. pub rewards: BoundedVec, - // TODO: perhaps I can add 'PeriodNumber' here so it's easy to identify expired rewards + // TODO: perhaps I can add 'PeriodNumber' here so it's easy to identify expired rewards? } impl, NT: Get> Default for DAppTierRewards { @@ -1629,3 +1659,34 @@ pub enum DAppTierError { /// Internal, unexpected error occured. InternalError, } + +/////////////////////////////////////////////////////////////////////// +//////////// MOVE THIS TO SOME PRIMITIVES CRATE LATER //////////// +/////////////////////////////////////////////////////////////////////// + +/// Interface for fetching price of the native token. +/// +/// TODO: discussion about below +/// The assumption is that the underlying implementation will ensure +/// this price is averaged and/or weighted over a certain period of time. +/// Alternative is to provide e.g. number of blocks for which an approximately averaged value is needed, +/// and let the underlying implementation take care converting block range into value best represening it. +pub trait PriceProvider { + /// Get the price of the native token. + fn average_price() -> FixedU64; +} + +pub trait RewardPoolProvider { + /// Get the reward pools for stakers and dApps. + /// + /// TODO: discussion about below + /// The assumption is that the underlying implementation keeps track of how often this is called. + /// E.g. let's assume it's supposed to be called at the end of each era. + /// In case era is forced, it will last shorter. If pallet is put into maintenance mode, era might last longer. + /// Reward should adjust to that accordingly. + /// Alternative is to provide number of blocks for which era lasted. + fn normal_reward_pools() -> (Balance, Balance); + + /// Get the bonus pool for stakers. + fn bonus_reward_pool() -> Balance; +} From 75586826babaf1ecb5e1a9ff2f7c7d0665631991 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 24 Oct 2023 16:51:43 +0200 Subject: [PATCH 22/86] fixes --- pallets/dapp-staking-v3/src/lib.rs | 1 - pallets/dapp-staking-v3/src/test/mock.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index afffe8ca94..e9efd320e6 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -46,7 +46,6 @@ use frame_support::{ weights::Weight, }; use frame_system::pallet_prelude::*; -use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ traits::{BadOrigin, Saturating, Zero}, Perbill, diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index a920b6a3dc..0c9ebb5426 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -233,7 +233,7 @@ impl ExtBuilder { pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); pallet_dapp_staking::NextTierConfig::::put(init_tier_config); - // TODO: include this into every test unless it explicitly doesn't need ot/ + // TODO: include this into every test unless it explicitly doesn't need it. // DappStaking::on_initialize(System::block_number()); } ); From e0f19e15d880d67c93afb4223943ef1443498a64 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 11:09:14 +0200 Subject: [PATCH 23/86] Extra calls --- pallets/dapp-staking-v3/src/lib.rs | 128 +++++++++++++----- pallets/dapp-staking-v3/src/test/mock.rs | 1 + .../dapp-staking-v3/src/test/testing_utils.rs | 11 ++ pallets/dapp-staking-v3/src/types.rs | 23 +++- 4 files changed, 131 insertions(+), 32 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e9efd320e6..eee209bcb5 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -139,6 +139,10 @@ pub mod pallet { #[pallet::constant] type UnlockingPeriod: Get>; + /// Maximum amount of stake entries contract is allowed to have at once. + #[pallet::constant] + type MaxNumberOfStakedContracts: Get; + /// Minimum amount staker can stake on a contract. #[pallet::constant] type MinimumStakeAmount: Get; @@ -308,6 +312,8 @@ pub mod pallet { InternalClaimDAppError, /// Contract is still active, not unregistered. ContractStillActive, + /// There are too many contract stake entries for the account. This can be cleaned up by either unstaking or cleaning expired entries. + TooManyStakedContracts, } /// General information about dApp staking protocol state. @@ -704,9 +710,6 @@ pub mod pallet { }, )?; - // As a consequence, this means that the sum of all stakes from `ContractStake` storage won't match - // total stake in active era info. This is fine, from the logic perspective, it doesn't cause any issues. - // TODO: left as potential discussion topic, remove later. ContractStake::::remove(&smart_contract); Self::deposit_event(Event::::DAppUnregistered { @@ -828,15 +831,23 @@ pub mod pallet { let amount = ledger.claim_unlocked(current_block); ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); + // In case it's full unlock, account is exiting dApp staking, ensure all storage is cleaned up. + // TODO: will be used after benchmarks + let _removed_entries = if ledger.is_empty() { + let _ = StakerInfo::::clear_prefix(&account, ledger.contract_stake_count, None); + ledger.contract_stake_count + } else { + 0 + }; + + // TODO: discussion point - it's possible that this will "kill" users ability to withdraw past rewards. + // This can be handled by the frontend though. + Self::update_ledger(&account, ledger); CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_removed(amount); }); - // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) - - // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have - Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); Ok(()) @@ -926,26 +937,44 @@ pub mod pallet { // This is ok since we only use this storage entry to keep track of how much each staker // has staked on each contract in the current period. We only ever need the latest information. // This is because `AccountLedger` is the one keeping information about how much was staked when. - let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { - Some(mut staking_info) - if staking_info.period_number() == protocol_state.period_number() => - { - staking_info.stake(amount, protocol_state.period_info.period_type); - staking_info - } - _ => { - ensure!( - amount >= T::MinimumStakeAmount::get(), - Error::::InsufficientStakeAmount - ); - let mut staking_info = SingularStakingInfo::new( - protocol_state.period_info.number, - protocol_state.period_info.period_type, - ); - staking_info.stake(amount, protocol_state.period_info.period_type); - staking_info - } - }; + let (mut new_staking_info, is_new_entry) = + match StakerInfo::::get(&account, &smart_contract) { + // Entry with matching period exists + Some(staking_info) + if staking_info.period_number() == protocol_state.period_number() => + { + (staking_info, false) + } + // Entry exists but period doesn't match + Some(_) => ( + SingularStakingInfo::new( + protocol_state.period_number(), + protocol_state.period_type(), + ), + false, + ), + // No entry exists + None => ( + SingularStakingInfo::new( + protocol_state.period_number(), + protocol_state.period_type(), + ), + true, + ), + }; + new_staking_info.stake(amount, protocol_state.period_type()); + ensure!( + new_staking_info.total_staked_amount() >= T::MinimumStakeAmount::get(), + Error::::InsufficientStakeAmount + ); + + if is_new_entry { + ledger.contract_stake_count.saturating_inc(); + ensure!( + ledger.contract_stake_count <= T::MaxNumberOfStakedContracts::get(), + Error::::TooManyStakedContracts + ); + } // 3. // Update `ContractStake` storage with the new stake amount on the specified contract. @@ -1068,15 +1097,17 @@ pub mod pallet { // 5. // Update remaining storage entries - Self::update_ledger(&account, ledger); ContractStake::::insert(&smart_contract, contract_stake_info); if new_staking_info.is_empty() { + ledger.contract_stake_count.saturating_dec(); StakerInfo::::remove(&account, &smart_contract); } else { StakerInfo::::insert(&account, &smart_contract, new_staking_info); } + Self::update_ledger(&account, ledger); + Self::deposit_event(Event::::Unstake { account, smart_contract, @@ -1098,9 +1129,6 @@ pub mod pallet { let protocol_state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); - // TODO: how do we handle expired rewards? Add an additional call to clean them up? - // Putting this logic inside existing calls will add even more complexity. - ensure!( !ledger.staker_rewards_claimed, Error::::StakerRewardsAlreadyClaimed @@ -1366,6 +1394,44 @@ pub mod pallet { Ok(()) } + + /// Used to unstake funds from a contract that was unregistered after an account staked on it. + #[pallet::call_index(15)] + #[pallet::weight(Weight::zero())] + pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResult { + // TODO: tests are missing but will be added later. + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let protocol_state = ActiveProtocolState::::get(); + let current_period = protocol_state.period_number(); + + // Find all entries which have expired. This is bounded by max allowed number of entries. + let to_be_deleted: Vec = StakerInfo::::iter_prefix(&account) + .filter_map(|(smart_contract, stake_info)| { + if stake_info.period_number() < current_period { + Some(smart_contract) + } else { + None + } + }) + .collect(); + + // Remove all expired entries. + for smart_contract in to_be_deleted { + StakerInfo::::remove(&account, &smart_contract); + } + + // Remove expired ledger stake entries, if needed. + let threshold_period = + current_period.saturating_sub(T::RewardRetentionInPeriods::get()); + let mut ledger = Ledger::::get(&account); + if ledger.maybe_cleanup_expired(threshold_period) { + Self::update_ledger(&account, ledger); + } + + Ok(()) + } } impl Pallet { diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 0c9ebb5426..d42874cb54 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -154,6 +154,7 @@ impl pallet_dapp_staking::Config for Test { type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU64<20>; + type MaxNumberOfStakedContracts = ConstU32<3>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 9e8d9318ea..c9980defe7 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -364,6 +364,17 @@ pub(crate) fn assert_claim_unlocked(account: AccountId) { post_snapshot.current_era_info.unlocking, pre_snapshot.current_era_info.unlocking - amount ); + + // In case of full withdrawal from the protocol + if post_ledger.is_empty() { + assert!(!Ledger::::contains_key(&account)); + assert!( + StakerInfo::::iter_prefix_values(&account) + .count() + .is_zero(), + "All stake entries need to be cleaned up." + ); + } } /// Claims the unlocked funds back into free balance of the user and assert success. diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 38b25fd045..282bc6b680 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -287,6 +287,7 @@ pub struct AccountLedger< UnlockingLen: Get, > { /// How much active locked amount an account has. + #[codec(compact)] pub locked: Balance, /// Vector of all the unlocking chunks. pub unlocking: BoundedVec, UnlockingLen>, @@ -302,7 +303,9 @@ pub struct AccountLedger< pub staker_rewards_claimed: bool, /// Indicator whether bonus rewards for the period has been claimed. pub bonus_reward_claimed: bool, - // TODO: introduce a variable which keeps track of on how many contracts this account has stake entries for. + /// Number of contract stake entries in storage. + #[codec(compact)] + pub contract_stake_count: u32, } impl Default for AccountLedger @@ -318,6 +321,7 @@ where staked_future: None, staker_rewards_claimed: false, bonus_reward_claimed: false, + contract_stake_count: Zero::zero(), } } } @@ -612,6 +616,23 @@ where } } + /// Cleanup staking information if it has expired. + /// + /// # Args + /// `threshold_period` - last period for which entries can still be considered valid. + /// + /// `true` if any change was made, `false` otherwise. + pub fn maybe_cleanup_expired(&mut self, threshold_period: PeriodNumber) -> bool { + match self.staked_period() { + Some(staked_period) if staked_period < threshold_period => { + self.staked = Default::default(); + self.staked_future = None; + true + } + _ => false, + } + } + /// 'Claim' rewards up to the specified era. /// Returns an iterator over the `(era, amount)` pairs, where `amount` /// describes the staked amount eligible for reward in the appropriate era. From 4926838d996fb2264b5e546500e35eb0bf8e4235 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 11:52:07 +0200 Subject: [PATCH 24/86] Refactoring --- pallets/dapp-staking-v3/src/lib.rs | 65 +++++++------------ .../dapp-staking-v3/src/test/testing_utils.rs | 43 +++++------- pallets/dapp-staking-v3/src/test/tests.rs | 26 ++++---- pallets/dapp-staking-v3/src/types.rs | 64 +----------------- 4 files changed, 57 insertions(+), 141 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index eee209bcb5..aae0dd082b 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -945,14 +945,10 @@ pub mod pallet { { (staking_info, false) } - // Entry exists but period doesn't match - Some(_) => ( - SingularStakingInfo::new( - protocol_state.period_number(), - protocol_state.period_type(), - ), - false, - ), + // Entry exists but period doesn't match. Either reward should be claimed or cleaned up. + Some(_) => { + return Err(Error::::UnclaimedRewardsFromPastPeriods.into()); + } // No entry exists None => ( SingularStakingInfo::new( @@ -1126,18 +1122,13 @@ pub mod pallet { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; - let protocol_state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); - - ensure!( - !ledger.staker_rewards_claimed, - Error::::StakerRewardsAlreadyClaimed - ); - - // Check if the rewards have expired let staked_period = ledger .staked_period() .ok_or(Error::::NoClaimableRewards)?; + + // Check if the rewards have expired + let protocol_state = ActiveProtocolState::::get(); ensure!( staked_period >= protocol_state @@ -1216,47 +1207,40 @@ pub mod pallet { /// Used to claim bonus reward for the eligible era, if applicable. #[pallet::call_index(12)] #[pallet::weight(Weight::zero())] - pub fn claim_bonus_reward(origin: OriginFor) -> DispatchResult { + pub fn claim_bonus_reward( + origin: OriginFor, + smart_contract: T::SmartContract, + ) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; + let staker_info = StakerInfo::::get(&account, &smart_contract) + .ok_or(Error::::NoClaimableRewards)?; let protocol_state = ActiveProtocolState::::get(); - let mut ledger = Ledger::::get(&account); + // Check if period has ended + let staked_period = staker_info.period_number(); ensure!( - !ledger.bonus_reward_claimed, - Error::::BonusRewardAlreadyClaimed + staked_period < protocol_state.period_number(), + Error::::NoClaimableRewards ); - // Check if the rewards have expired - let staked_period = ledger - .staked_period() - .ok_or(Error::::NoClaimableRewards)?; ensure!( - staked_period + staker_info.is_loyal(), + Error::::NotEligibleForBonusReward + ); + ensure!( + staker_info.period_number() >= protocol_state .period_number() .saturating_sub(T::RewardRetentionInPeriods::get()), Error::::RewardExpired ); - // Check if period has ended - ensure!( - staked_period < protocol_state.period_number(), - Error::::NoClaimableRewards - ); - // Check if user is applicable for bonus reward - let eligible_amount = - ledger - .claim_bonus_reward(staked_period) - .map_err(|err| match err { - AccountLedgerError::NothingToClaim => Error::::NoClaimableRewards, - _ => Error::::InternalClaimBonusError, - })?; + let eligible_amount = staker_info.staked_amount(PeriodType::Voting); let period_end_info = PeriodEnd::::get(&staked_period).ok_or(Error::::InternalClaimBonusError)?; - // Defensive check - we should never get this far in function if no voting period stake exists. ensure!( !period_end_info.total_vp_stake.is_zero(), @@ -1271,7 +1255,8 @@ pub mod pallet { T::Currency::deposit_into_existing(&account, bonus_reward) .map_err(|_| Error::::InternalClaimStakerError)?; - Self::update_ledger(&account, ledger); + // Cleanup entry since the reward has been claimed + StakerInfo::::remove(&account, &smart_contract); Self::deposit_event(Event::::BonusReward { account: account.clone(), diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index c9980defe7..9cb1463be3 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -848,13 +848,9 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let post_snapshot = MemorySnapshot::new(); let post_ledger = post_snapshot.ledger.get(&account).unwrap(); - if is_full_claim && pre_ledger.bonus_reward_claimed { + if is_full_claim { assert_eq!(post_ledger.staked, StakeAmount::default()); assert!(post_ledger.staked_future.is_none()); - assert!(!post_ledger.staker_rewards_claimed); - assert!(!post_ledger.bonus_reward_claimed); - } else if is_full_claim { - assert!(post_ledger.staker_rewards_claimed); } else { assert_eq!(post_ledger.staked.era, last_claim_era + 1); assert!(post_ledger.staked_future.is_none()); @@ -862,16 +858,17 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { } /// Claim staker rewards. -pub(crate) fn assert_claim_bonus_reward(account: AccountId) { +pub(crate) fn assert_claim_bonus_reward(account: AccountId, smart_contract: &MockSmartContract) { let pre_snapshot = MemorySnapshot::new(); - let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_staker_info = pre_snapshot + .staker_info + .get(&(account, *smart_contract)) + .unwrap(); let pre_total_issuance = ::Currency::total_issuance(); let pre_free_balance = ::Currency::free_balance(&account); - let staked_period = pre_ledger - .staked_period() - .expect("Must have a staked period."); - let stake_amount = pre_ledger.staked_amount_for_type(PeriodType::Voting, staked_period); + let staked_period = pre_staker_info.period_number(); + let stake_amount = pre_staker_info.staked_amount(PeriodType::Voting); let period_end_info = pre_snapshot .period_end @@ -882,9 +879,10 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId) { * period_end_info.bonus_reward_pool; // Claim bonus reward & verify event - assert_ok!(DappStaking::claim_bonus_reward(RuntimeOrigin::signed( - account - ),)); + assert_ok!(DappStaking::claim_bonus_reward( + RuntimeOrigin::signed(account), + smart_contract.clone(), + )); System::assert_last_event(RuntimeEvent::DappStaking(Event::BonusReward { account, period: staked_period, @@ -907,19 +905,10 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId) { "Free balance must increase by the reward amount." ); - let post_snapshot = MemorySnapshot::new(); - let post_ledger = post_snapshot.ledger.get(&account).unwrap(); - - // All rewards for period have been claimed - if pre_ledger.staker_rewards_claimed { - assert_eq!(post_ledger.staked, StakeAmount::default()); - assert!(post_ledger.staked_future.is_none()); - assert!(!post_ledger.staker_rewards_claimed); - assert!(!post_ledger.bonus_reward_claimed); - } else { - // Staker still has some staker rewards remaining - assert!(post_ledger.bonus_reward_claimed); - } + assert!( + !StakerInfo::::contains_key(&account, smart_contract), + "Entry must be removed after successful reward claim." + ); } /// Claim dapp reward for a particular era. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 3cdcbfa2d5..0f5801de52 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1179,7 +1179,7 @@ fn claim_staker_rewards_double_call_fails() { assert_noop!( DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), - Error::::StakerRewardsAlreadyClaimed, + Error::::NoClaimableRewards, ); }) } @@ -1285,7 +1285,7 @@ fn claim_bonus_reward_works() { // 1st scenario - advance to the next period, first claim bonus reward, then staker rewards advance_to_next_period(); - assert_claim_bonus_reward(account); + assert_claim_bonus_reward(account, &smart_contract); for _ in 0..required_number_of_reward_claims(account) { assert_claim_staker_rewards(account); } @@ -1297,10 +1297,10 @@ fn claim_bonus_reward_works() { assert_claim_staker_rewards(account); } assert!( - Ledger::::get(&account).staker_rewards_claimed, + Ledger::::get(&account).staked.is_empty(), "Sanity check." ); - assert_claim_bonus_reward(account); + assert_claim_bonus_reward(account, &smart_contract); }) } @@ -1320,11 +1320,11 @@ fn claim_bonus_reward_double_call_fails() { // Advance to the next period, claim bonus reward, then try to do it again advance_to_next_period(); - assert_claim_bonus_reward(account); + assert_claim_bonus_reward(account, &smart_contract); assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), - Error::::BonusRewardAlreadyClaimed, + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::NoClaimableRewards, ); }) } @@ -1343,7 +1343,7 @@ fn claim_bonus_reward_when_nothing_to_claim_fails() { // 1st - try to claim bonus reward when no stake is present assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), Error::::NoClaimableRewards, ); @@ -1351,7 +1351,7 @@ fn claim_bonus_reward_when_nothing_to_claim_fails() { let stake_amount = 93; assert_stake(account, &smart_contract, stake_amount); assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), Error::::NoClaimableRewards, ); }) @@ -1381,8 +1381,8 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { advance_to_next_period(); assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), - Error::::NoClaimableRewards, + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::NotEligibleForBonusReward, ); }) } @@ -1406,7 +1406,7 @@ fn claim_bonus_reward_after_expiry_fails() { advance_to_period( ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); - assert_claim_bonus_reward(account); + assert_claim_bonus_reward(account, &smart_contract); for _ in 0..required_number_of_reward_claims(account) { assert_claim_staker_rewards(account); } @@ -1417,7 +1417,7 @@ fn claim_bonus_reward_after_expiry_fails() { ActiveProtocolState::::get().period_number() + reward_retention_in_periods + 1, ); assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account)), + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), Error::::RewardExpired, ); }) diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 282bc6b680..04b3366915 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -299,10 +299,6 @@ pub struct AccountLedger< /// Both `stake` and `staked_future` must ALWAYS refer to the same period. /// If `staked_future` is `Some`, it will always be **EXACTLY** one era after the `staked` field era. pub staked_future: Option, - /// Indicator whether staker rewards for the entire period have been claimed. - pub staker_rewards_claimed: bool, - /// Indicator whether bonus rewards for the period has been claimed. - pub bonus_reward_claimed: bool, /// Number of contract stake entries in storage. #[codec(compact)] pub contract_stake_count: u32, @@ -319,8 +315,6 @@ where unlocking: BoundedVec::default(), staked: StakeAmount::default(), staked_future: None, - staker_rewards_claimed: false, - bonus_reward_claimed: false, contract_stake_count: Zero::zero(), } } @@ -462,7 +456,6 @@ where } /// How much is staked for the specified period type, in respect to the specified era. - // TODO: if period is force-ended, this could be abused to get bigger rewards. I need to make the check more strict when claiming bonus rewards! pub fn staked_amount_for_type(&self, period_type: PeriodType, period: PeriodNumber) -> Balance { // First check the 'future' entry, afterwards check the 'first' entry match self.staked_future { @@ -643,10 +636,6 @@ where era: EraNumber, period_end: Option, ) -> Result { - if self.staker_rewards_claimed { - return Err(AccountLedgerError::AlreadyClaimed); - } - // Main entry exists, but era isn't 'in history' if !self.staked.is_empty() && era <= self.staked.era { return Err(AccountLedgerError::NothingToClaim); @@ -682,64 +671,17 @@ where } self.staked.era = era.saturating_add(1); - // Make sure to clean up the entries + // Make sure to clean up the entries if all rewards for the period have been claimed. match period_end { Some(ending_era) if era >= ending_era => { - self.staker_rewards_claimed = true; - self.maybe_cleanup_stake_entries(); + self.staked = Default::default(); + self.staked_future = None; } _ => (), } Ok(result) } - - /// Claim bonus reward, if possible. - /// - /// Returns the amount eligible for bonus reward calculation, or an error. - pub fn claim_bonus_reward( - &mut self, - period: PeriodNumber, - ) -> Result { - if self.bonus_reward_claimed { - return Err(AccountLedgerError::AlreadyClaimed); - } - - let amount = self.staked_amount_for_type(PeriodType::Voting, period); - - if amount.is_zero() { - Err(AccountLedgerError::NothingToClaim) - } else { - self.bonus_reward_claimed = true; - self.maybe_cleanup_stake_entries(); - Ok(amount) - } - } - - /// Cleanup fields related to stake & rewards, in case all possible rewards for the current state - /// have been claimed. - fn maybe_cleanup_stake_entries(&mut self) { - // Stake rewards are either all claimed, or there's nothing to claim. - let stake_cleanup = - self.staker_rewards_claimed || (self.staked.is_empty() && self.staked_future.is_none()); - - // Bonus reward might be covered by the 'future' entry. - let has_future_entry_stake = if let Some(stake_amount) = self.staked_future { - !stake_amount.voting.is_zero() - } else { - false - }; - // Either rewards have already been claimed, or there are no possible bonus rewards to claim. - let bonus_cleanup = - self.bonus_reward_claimed || self.staked.voting.is_zero() && !has_future_entry_stake; - - if stake_cleanup && bonus_cleanup { - self.staked = Default::default(); - self.staked_future = None; - self.staker_rewards_claimed = false; - self.bonus_reward_claimed = false; - } - } } /// Helper internal struct for iterating over `(era, stake amount)` pairs. From d47bcf49d0697e479ab8c216e067cca32364db90 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 12:31:47 +0200 Subject: [PATCH 25/86] More refactoring, improvements, TODO solving --- pallets/dapp-staking-v3/src/lib.rs | 48 ++++++++++--------- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- .../dapp-staking-v3/src/test/testing_utils.rs | 3 +- pallets/dapp-staking-v3/src/test/tests.rs | 27 +++++++++++ .../dapp-staking-v3/src/test/tests_types.rs | 3 -- pallets/dapp-staking-v3/src/types.rs | 19 ++++++-- 6 files changed, 71 insertions(+), 31 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index aae0dd082b..7596bfe0df 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -223,6 +223,7 @@ pub mod pallet { }, BonusReward { account: T::AccountId, + smart_contract: T::SmartContract, period: PeriodNumber, amount: Balance, }, @@ -289,14 +290,10 @@ pub mod pallet { InternalUnstakeError, /// Rewards are no longer claimable since they are too old. RewardExpired, - /// All staker rewards for the period have been claimed. - StakerRewardsAlreadyClaimed, /// There are no claimable rewards. NoClaimableRewards, /// An unexpected error occured while trying to claim staker rewards. InternalClaimStakerError, - /// Bonus rewards have already been claimed. - BonusRewardAlreadyClaimed, /// Account is has no eligible stake amount for bonus reward. NotEligibleForBonusReward, /// An unexpected error occured while trying to claim bonus reward. @@ -840,7 +837,7 @@ pub mod pallet { 0 }; - // TODO: discussion point - it's possible that this will "kill" users ability to withdraw past rewards. + // TODO: discussion point - this will "kill" users ability to withdraw past rewards. // This can be handled by the frontend though. Self::update_ledger(&account, ledger); @@ -1130,10 +1127,7 @@ pub mod pallet { // Check if the rewards have expired let protocol_state = ActiveProtocolState::::get(); ensure!( - staked_period - >= protocol_state - .period_number() - .saturating_sub(T::RewardRetentionInPeriods::get()), + staked_period >= Self::oldest_claimable_period(protocol_state.period_number()), Error::::RewardExpired ); @@ -1203,8 +1197,7 @@ pub mod pallet { Ok(()) } - // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. - /// Used to claim bonus reward for the eligible era, if applicable. + /// Used to claim bonus reward for a smart contract, if eligible. #[pallet::call_index(12)] #[pallet::weight(Weight::zero())] pub fn claim_bonus_reward( @@ -1218,7 +1211,10 @@ pub mod pallet { .ok_or(Error::::NoClaimableRewards)?; let protocol_state = ActiveProtocolState::::get(); - // Check if period has ended + // Ensure: + // 1. Period for which rewards are being claimed has ended. + // 2. Account has been a loyal staker. + // 3. Rewards haven't expired. let staked_period = staker_info.period_number(); ensure!( staked_period < protocol_state.period_number(), @@ -1230,15 +1226,10 @@ pub mod pallet { ); ensure!( staker_info.period_number() - >= protocol_state - .period_number() - .saturating_sub(T::RewardRetentionInPeriods::get()), + >= Self::oldest_claimable_period(protocol_state.period_number()), Error::::RewardExpired ); - // Check if user is applicable for bonus reward - let eligible_amount = staker_info.staked_amount(PeriodType::Voting); - let period_end_info = PeriodEnd::::get(&staked_period).ok_or(Error::::InternalClaimBonusError)?; // Defensive check - we should never get this far in function if no voting period stake exists. @@ -1247,6 +1238,7 @@ pub mod pallet { Error::::InternalClaimBonusError ); + let eligible_amount = staker_info.staked_amount(PeriodType::Voting); let bonus_reward = Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; @@ -1260,6 +1252,7 @@ pub mod pallet { Self::deposit_event(Event::::BonusReward { account: account.clone(), + smart_contract, period: staked_period, amount: bonus_reward, }); @@ -1287,6 +1280,11 @@ pub mod pallet { // 'Consume' dApp reward for the specified era, if possible. let mut dapp_tiers = DAppTiers::::get(&era).ok_or(Error::::NoDAppTierInfo)?; + ensure!( + Self::oldest_claimable_period(dapp_tiers.period) <= protocol_state.period_number(), + Error::::RewardExpired + ); + let (amount, tier_id) = dapp_tiers .try_consume(dapp_info.id) @@ -1408,8 +1406,7 @@ pub mod pallet { } // Remove expired ledger stake entries, if needed. - let threshold_period = - current_period.saturating_sub(T::RewardRetentionInPeriods::get()); + let threshold_period = Self::oldest_claimable_period(current_period); let mut ledger = Ledger::::get(&account); if ledger.maybe_cleanup_expired(threshold_period) { Self::update_ledger(&account, ledger); @@ -1476,6 +1473,12 @@ pub mod pallet { era.saturating_sub(era % T::EraRewardSpanLength::get()) } + /// Return the oldest period for which rewards can be claimed. + /// All rewards before that period are considered to be expired. + pub fn oldest_claimable_period(current_period: PeriodNumber) -> PeriodNumber { + current_period.saturating_sub(T::RewardRetentionInPeriods::get()) + } + /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. /// /// The returned object contains information about each dApp that made it into a tier. @@ -1541,7 +1544,7 @@ pub mod pallet { // 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_accrue(1); + global_idx.saturating_inc(); dapp_tiers.push(DAppTier { dapp_id: *dapp_id, tier_id: Some(tier_id), @@ -1551,7 +1554,7 @@ pub mod pallet { } } - tier_id.saturating_accrue(1); + tier_id.saturating_inc(); } // 4. @@ -1571,6 +1574,7 @@ pub mod pallet { DAppTierRewards::, T::NumberOfTiers>::new( dapp_tiers, tier_rewards, + period, ) .unwrap_or_default() } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index d42874cb54..6de7dc0b61 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -199,7 +199,7 @@ impl ExtBuilder { maintenance: false, }); - // TODO: improve this laterm should be handled via genesis? + // TODO: improve this later, should be handled via genesis? let tier_params = TierParameters::<::NumberOfTiers> { reward_portion: BoundedVec::try_from(vec![ Permill::from_percent(40), diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 9cb1463be3..c3c06f8534 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -484,7 +484,7 @@ pub(crate) fn assert_stake( pre_ledger.stakeable_amount(stake_period) - amount, "Stakeable amount must decrease by the 'amount'" ); - // TODO: maybe expand checks here? + // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -885,6 +885,7 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId, smart_contract: &Moc )); System::assert_last_event(RuntimeEvent::DappStaking(Event::BonusReward { account, + smart_contract: *smart_contract, period: staked_period, amount: reward, })); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 0f5801de52..ab33654c49 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -102,6 +102,33 @@ fn maintenace_mode_call_filtering_works() { DappStaking::unstake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), Error::::Disabled ); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(1), MockSmartContract::default()), + Error::::Disabled + ); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(1), + MockSmartContract::default(), + 1 + ), + Error::::Disabled + ); + assert_noop!( + DappStaking::unstake_from_unregistered( + RuntimeOrigin::signed(1), + MockSmartContract::default() + ), + Error::::Disabled + ); + assert_noop!( + DappStaking::cleanup_expired_entries(RuntimeOrigin::signed(1)), + Error::::Disabled + ); }) } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 62cd148911..1211792180 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -335,9 +335,6 @@ fn account_ledger_add_stake_amount_basic_example_works() { .is_ok()); assert!(acc_ledger.staked.is_empty()); assert!(acc_ledger.staked_future.is_none()); - assert!(acc_ledger - .staked_amount_for_type(PeriodType::Voting, period_number) - .is_zero()); // 1st scenario - stake some amount, and ensure values are as expected. let first_era = 1; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 04b3366915..fec4351457 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -740,7 +740,7 @@ impl Iterator for EraStakePairIter { // Afterwards, just keep returning the same amount for different eras if self.start_era <= self.end_era { let value = (self.start_era, self.amount); - self.start_era.saturating_accrue(1); + self.start_era.saturating_inc(); return Some(value); } else { None @@ -1559,7 +1559,9 @@ pub struct DAppTierRewards, NT: Get> { pub dapps: BoundedVec, /// Rewards for each tier. First entry refers to the first tier, and so on. pub rewards: BoundedVec, - // TODO: perhaps I can add 'PeriodNumber' here so it's easy to identify expired rewards? + /// Period during which this struct was created. + #[codec(compact)] + pub period: PeriodNumber, } impl, NT: Get> Default for DAppTierRewards { @@ -1567,6 +1569,7 @@ impl, NT: Get> Default for DAppTierRewards { Self { dapps: BoundedVec::default(), rewards: BoundedVec::default(), + period: 0, } } } @@ -1574,10 +1577,18 @@ impl, NT: Get> Default for DAppTierRewards { 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 { + pub fn new( + dapps: Vec, + rewards: Vec, + period: PeriodNumber, + ) -> Result { let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; - Ok(Self { dapps, rewards }) + Ok(Self { + dapps, + rewards, + period, + }) } /// Consume reward for the specified dapp id, returning its amount and tier Id. From 0197c954f456a500a47b59669a2326cc95c82347 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 14:56:49 +0200 Subject: [PATCH 26/86] Local integration --- Cargo.lock | 2 + Cargo.toml | 1 + pallets/dapp-staking-v3/src/lib.rs | 15 ++++++-- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- pallets/dapp-staking-v3/src/test/tests.rs | 6 +-- pallets/dapp-staking-v3/src/types.rs | 1 + runtime/local/Cargo.toml | 4 ++ runtime/local/src/lib.rs | 45 ++++++++++++++++++++++- 8 files changed, 68 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 536ec19859..19c286fc40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5524,6 +5524,7 @@ dependencies = [ "pallet-contracts", "pallet-contracts-primitives", "pallet-custom-signatures", + "pallet-dapp-staking-v3", "pallet-dapps-staking", "pallet-democracy", "pallet-ethereum", @@ -5558,6 +5559,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-api", + "sp-arithmetic", "sp-block-builder", "sp-consensus-aura", "sp-core", diff --git a/Cargo.toml b/Cargo.toml index fd6f3ee412..91789f677a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -274,6 +274,7 @@ pallet-block-reward = { path = "./pallets/block-reward", default-features = fals pallet-collator-selection = { path = "./pallets/collator-selection", default-features = false } pallet-custom-signatures = { path = "./pallets/custom-signatures", default-features = false } pallet-dapps-staking = { path = "./pallets/dapps-staking", default-features = false } +pallet-dapp-staking-v3 = { path = "./pallets/dapp-staking-v3", default-features = false } pallet-xc-asset-config = { path = "./pallets/xc-asset-config", default-features = false } pallet-xvm = { path = "./pallets/xvm", default-features = false } pallet-xcm = { path = "./pallets/pallet-xcm", default-features = false } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 7596bfe0df..2bfcb74ace 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -50,10 +50,13 @@ use sp_runtime::{ traits::{BadOrigin, Saturating, Zero}, Perbill, }; +pub use sp_std::vec::Vec; use astar_primitives::Balance; use crate::types::*; +pub use crate::types::{PriceProvider, RewardPoolProvider}; + pub use pallet::*; #[cfg(test)] @@ -135,9 +138,10 @@ pub mod pallet { #[pallet::constant] type MinimumLockedAmount: Get; - /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. + /// Number of standard eras that need to pass before unlocking chunk can be claimed. + /// Even though it's expressed in 'eras', it's actually measured in number of blocks. #[pallet::constant] - type UnlockingPeriod: Get>; + type UnlockingPeriod: Get; /// Maximum amount of stake entries contract is allowed to have at once. #[pallet::constant] @@ -796,7 +800,7 @@ pub mod pallet { ledger.subtract_lock_amount(amount_to_unlock); let current_block = frame_system::Pallet::::block_number(); - let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); + let unlock_block = current_block.saturating_add(Self::unlock_period()); ledger .add_unlocking_chunk(amount_to_unlock, unlock_block) .map_err(|_| Error::::TooManyUnlockingChunks)?; @@ -1479,6 +1483,11 @@ pub mod pallet { current_period.saturating_sub(T::RewardRetentionInPeriods::get()) } + /// Unlocking period expressed in the number of blocks. + pub fn unlock_period() -> BlockNumberFor { + T::StandardEraLength::get().saturating_mul(T::UnlockingPeriod::get().into()) + } + /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. /// /// The returned object contains information about each dApp that made it into a tier. diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 6de7dc0b61..e788349707 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -153,7 +153,7 @@ impl pallet_dapp_staking::Config for Test { type MaxNumberOfContracts = ConstU16<10>; type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; - type UnlockingPeriod = ConstU64<20>; + type UnlockingPeriod = ConstU32<2>; type MaxNumberOfStakedContracts = ConstU32<3>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index ab33654c49..a823fc1abc 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -641,7 +641,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { #[test] fn claim_unlocked_is_ok() { ExtBuilder::build().execute_with(|| { - let unlocking_blocks: BlockNumber = ::UnlockingPeriod::get(); + let unlocking_blocks = DappStaking::unlock_period(); // Lock some amount in a few eras let account = 2; @@ -691,7 +691,7 @@ fn claim_unlocked_no_eligible_chunks_fails() { // Cannot claim if unlock period hasn't passed yet let lock_amount = 103; assert_lock(account, lock_amount); - let unlocking_blocks: BlockNumber = ::UnlockingPeriod::get(); + let unlocking_blocks = DappStaking::unlock_period(); run_for_blocks(unlocking_blocks - 1); assert_noop!( DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), @@ -769,7 +769,7 @@ fn relock_unlocking_insufficient_lock_amount_fails() { }); // Make sure only one chunk is left - let unlocking_blocks: BlockNumber = ::UnlockingPeriod::get(); + let unlocking_blocks = DappStaking::unlock_period(); run_for_blocks(unlocking_blocks - 1); assert_claim_unlocked(account); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index fec4351457..834cb6ea34 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -26,6 +26,7 @@ use sp_runtime::{ traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; +pub use sp_std::vec::Vec; use astar_primitives::Balance; diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 44012e1760..a1280f19d6 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -48,6 +48,7 @@ pallet-treasury = { workspace = true } pallet-utility = { workspace = true } pallet-vesting = { workspace = true } sp-api = { workspace = true } +sp-arithmetic = { workspace = true } sp-block-builder = { workspace = true } sp-consensus-aura = { workspace = true } sp-core = { workspace = true } @@ -70,6 +71,7 @@ pallet-block-reward = { workspace = true } pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-xvm = { workspace = true } pallet-custom-signatures = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } pallet-evm-precompile-dapps-staking = { workspace = true } @@ -117,6 +119,7 @@ std = [ "pallet-chain-extension-xvm/std", "pallet-custom-signatures/std", "pallet-dapps-staking/std", + "pallet-dapp-staking-v3/std", "pallet-base-fee/std", "pallet-ethereum/std", "pallet-evm/std", @@ -150,6 +153,7 @@ std = [ "sp-offchain/std", "sp-runtime/std", "sp-session/std", + "sp-arithmetic/std", "sp-std/std", "sp-transaction-pool/std", "sp-version/std", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 40c1e7e9a3..addebe80e1 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -27,7 +27,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, + AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, }, weights::{ @@ -46,6 +46,7 @@ use pallet_evm_precompile_assets_erc20::AddressToAssetId; use pallet_grandpa::{fg_primitives, AuthorityList as GrandpaAuthorityList}; use parity_scale_codec::{Compact, Decode, Encode, MaxEncodedLen}; use sp_api::impl_runtime_apis; +use sp_arithmetic::fixed_point::FixedU64; use sp_core::{crypto::KeyTypeId, ConstBool, OpaqueMetadata, H160, H256, U256}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, @@ -439,6 +440,47 @@ impl> From<[u8; 32]> for SmartContract { } } +pub struct DummyPriceProvider; +impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyRewardPoolProvider; +impl pallet_dapp_staking_v3::RewardPoolProvider for DummyRewardPoolProvider { + fn normal_reward_pools() -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000 * AST), + Balance::from(1_000_000_000 * AST), + ) + } + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000 * AST) + } +} + +impl pallet_dapp_staking_v3::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type SmartContract = SmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type RewardPoolProvider = DummyRewardPoolProvider; + type StandardEraLength = ConstU32<30>; // should be 1 minute per standard era + type StandardErasPerVotingPeriod = ConstU32<2>; + type StandardErasPerBuildAndEarnPeriod = ConstU32<10>; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU16<10>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128; + type UnlockingPeriod = ConstU32<2>; + type MaxNumberOfStakedContracts = ConstU32<3>; + type MinimumStakeAmount = ConstU128; + type NumberOfTiers = ConstU32<4>; +} + impl pallet_utility::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -1005,6 +1047,7 @@ construct_runtime!( Balances: pallet_balances, Vesting: pallet_vesting, DappsStaking: pallet_dapps_staking, + DappStaking: pallet_dapp_staking_v3, BlockReward: pallet_block_reward, TransactionPayment: pallet_transaction_payment, EVM: pallet_evm, From 4ea8556c37531853f921d329cbe98fedb6f1c842 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 16:11:02 +0200 Subject: [PATCH 27/86] Genesis config --- Cargo.lock | 1 + bin/collator/Cargo.toml | 1 + bin/collator/src/local/chain_spec.rs | 40 ++++++++++-- pallets/dapp-staking-v3/src/lib.rs | 77 ++++++++++++++++++++-- pallets/dapp-staking-v3/src/test/mock.rs | 8 ++- pallets/dapp-staking-v3/src/types.rs | 81 +++++++++++++++++++++--- 6 files changed, 187 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19c286fc40..ab6f066763 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "moonbeam-rpc-trace", "moonbeam-rpc-txpool", "pallet-block-reward", + "pallet-dapp-staking-v3", "pallet-ethereum", "pallet-evm", "pallet-transaction-payment", diff --git a/bin/collator/Cargo.toml b/bin/collator/Cargo.toml index 3c7fccb18e..d8f46e7b2a 100644 --- a/bin/collator/Cargo.toml +++ b/bin/collator/Cargo.toml @@ -95,6 +95,7 @@ shiden-runtime = { workspace = true, features = ["std"] } # astar pallets dependencies astar-primitives = { workspace = true } pallet-block-reward = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } # frame dependencies frame-system = { workspace = true, features = ["std"] } diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index 8d56c8747c..4774784754 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -20,17 +20,19 @@ use local_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, BalancesConfig, BaseFeeConfig, - BlockRewardConfig, CouncilConfig, DemocracyConfig, EVMConfig, GenesisConfig, GrandpaConfig, - GrandpaId, Precompiles, Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, - TreasuryConfig, VestingConfig, + BlockRewardConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMConfig, GenesisConfig, + GrandpaConfig, GrandpaId, Precompiles, Signature, SudoConfig, SystemConfig, + TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, }; use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Perbill, + Perbill, Permill, }; +use pallet_dapp_staking_v3::TierThreshold; + type AccountPublic = ::Signer; /// Specialized `ChainSpec` for Shiden Network. @@ -181,6 +183,36 @@ fn testnet_genesis( }, democracy: DemocracyConfig::default(), treasury: TreasuryConfig::default(), + dapp_staking: DappStakingConfig { + reward_portion: vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ], + slot_distribution: vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ], + tier_thresholds: vec![ + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 80, + }, + TierThreshold::DynamicTvlAmount { + amount: 50, + minimum_amount: 40, + }, + TierThreshold::DynamicTvlAmount { + amount: 20, + minimum_amount: 20, + }, + TierThreshold::FixedTvlAmount { amount: 10 }, + ], + slots_per_tier: vec![10, 20, 30, 40], + }, } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 2bfcb74ace..823266f26b 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -48,21 +48,19 @@ use frame_support::{ use frame_system::pallet_prelude::*; use sp_runtime::{ traits::{BadOrigin, Saturating, Zero}, - Perbill, + Perbill, Permill, }; pub use sp_std::vec::Vec; use astar_primitives::Balance; -use crate::types::*; -pub use crate::types::{PriceProvider, RewardPoolProvider}; - pub use pallet::*; #[cfg(test)] mod test; mod types; +pub use types::*; // TODO: maybe make it more restrictive later const STAKING_ID: LockIdentifier = *b"dapstake"; @@ -401,6 +399,77 @@ pub mod pallet { pub type DAppTiers = StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub reward_portion: Vec, + pub slot_distribution: Vec, + pub tier_thresholds: Vec, + pub slots_per_tier: Vec, + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + // Prepare tier parameters & verify their correctness + let tier_params = TierParameters:: { + reward_portion: BoundedVec::::try_from( + self.reward_portion.clone(), + ) + .expect("Invalid number of reward portions provided."), + slot_distribution: BoundedVec::::try_from( + self.slot_distribution.clone(), + ) + .expect("Invalid number of slot distributions provided."), + tier_thresholds: BoundedVec::::try_from( + self.tier_thresholds.clone(), + ) + .expect("Invalid number of tier thresholds provided."), + }; + assert!( + tier_params.is_valid(), + "Invalid tier parameters values provided." + ); + + // Prepare tier configuration and verify its correctness + let number_of_slots = self + .slots_per_tier + .iter() + .fold(0, |acc, &slots| acc + slots); + let tier_config = TiersConfiguration:: { + number_of_slots, + slots_per_tier: BoundedVec::::try_from( + self.slots_per_tier.clone(), + ) + .expect("Invalid number of slots per tier entries provided."), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + assert!( + tier_params.is_valid(), + "Invalid tier config values provided." + ); + + // Prepare initial protocol state + let protocol_state = ProtocolState { + era: 1, + next_era_start: Pallet::::blocks_per_voting_period() + 1_u32.into(), + period_info: PeriodInfo { + number: 1, + period_type: PeriodType::Voting, + ending_era: 2, + }, + maintenance: false, + }; + + // Initialize necessary storage items + ActiveProtocolState::::put(protocol_state); + StaticTierParams::::put(tier_params); + TierConfig::::put(tier_config.clone()); + NextTierConfig::::put(tier_config); + } + } + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(now: BlockNumberFor) -> Weight { diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index e788349707..6b05742d93 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -180,14 +180,14 @@ impl ExtBuilder { ext.execute_with(|| { System::set_block_number(1); - // TODO: not sure why the mess with type happens here, I can check it later + // Not sure why the mess with type happens here, but trait specification is needed to compile let era_length: BlockNumber = <::StandardEraLength as sp_core::Get<_>>::get(); let voting_period_length_in_eras: EraNumber = <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( ); - // TODO: handle this via GenesisConfig, and some helper functions to set the state + // Init protocol state pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { era: 1, next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, @@ -199,7 +199,7 @@ impl ExtBuilder { maintenance: false, }); - // TODO: improve this later, should be handled via genesis? + // Init tier params let tier_params = TierParameters::<::NumberOfTiers> { reward_portion: BoundedVec::try_from(vec![ Permill::from_percent(40), @@ -223,6 +223,8 @@ impl ExtBuilder { ]) .unwrap(), }; + + // Init tier config, based on the initial params let init_tier_config = TiersConfiguration::<::NumberOfTiers> { number_of_slots: 100, slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 834cb6ea34..08320b29e6 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -26,7 +26,7 @@ use sp_runtime::{ traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; -pub use sp_std::vec::Vec; +pub use sp_std::{fmt::Debug, vec::Vec}; use astar_primitives::Balance; @@ -281,10 +281,19 @@ where } /// General info about user's stakes -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] #[scale_info(skip_type_params(UnlockingLen))] pub struct AccountLedger< - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, > { /// How much active locked amount an account has. @@ -307,7 +316,7 @@ pub struct AccountLedger< impl Default for AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, { fn default() -> Self { @@ -323,7 +332,7 @@ where impl AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, { /// Empty if no locked/unlocking/staked info exists. @@ -1006,7 +1015,17 @@ impl SingularStakingInfo { const STAKING_SERIES_HISTORY: u32 = 3; /// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, + Default, +)] pub struct ContractStakeAmountSeries(BoundedVec>); impl ContractStakeAmountSeries { /// Helper function to create a new instance of `ContractStakeAmountSeries`. @@ -1246,7 +1265,16 @@ pub enum EraRewardSpanError { } /// Used to efficiently store era span information. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] #[scale_info(skip_type_params(SL))] pub struct EraRewardSpan> { /// Span of EraRewardInfo entries. @@ -1333,6 +1361,7 @@ where /// Description of tier entry requirement. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub enum TierThreshold { /// Entry into tier is mandated by minimum amount of staked funds. /// Value is fixed, and is not expected to change in between periods. @@ -1357,7 +1386,16 @@ impl TierThreshold { } /// Top level description of tier slot parameters used to calculate tier configuration. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] #[scale_info(skip_type_params(NT))] pub struct TierParameters> { /// Reward distribution per tier, in percentage. @@ -1381,6 +1419,8 @@ impl> TierParameters { number_of_tiers == self.reward_portion.len() && number_of_tiers == self.slot_distribution.len() && number_of_tiers == self.tier_thresholds.len() + + // TODO: Make check more detailed, verify that entries sum up to 1 or 100% } } @@ -1397,7 +1437,16 @@ 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)] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] #[scale_info(skip_type_params(NT))] pub struct TiersConfiguration> { /// Total number of slots. @@ -1553,7 +1602,16 @@ pub struct DAppTier { } /// Information about all of the dApps that got into tiers, and tier rewards -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + 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) @@ -1651,6 +1709,9 @@ pub trait PriceProvider { fn average_price() -> FixedU64; } +// TODO: however the implementation ends up looking, +// it should consider total staked amount when filling up the bonus pool. +// This is to ensure bonus rewards aren't too large in case there is little amount of staked funds. pub trait RewardPoolProvider { /// Get the reward pools for stakers and dApps. /// From 589575bbe140f8f8e7435fc7814535a7a821bf04 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 16:36:21 +0200 Subject: [PATCH 28/86] Add forcing call --- pallets/dapp-staking-v3/src/lib.rs | 42 +++++++++++++++++++---- pallets/dapp-staking-v3/src/test/tests.rs | 8 +++-- pallets/dapp-staking-v3/src/types.rs | 12 ++++--- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 823266f26b..7732d0c61a 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -47,7 +47,7 @@ use frame_support::{ }; use frame_system::pallet_prelude::*; use sp_runtime::{ - traits::{BadOrigin, Saturating, Zero}, + traits::{BadOrigin, One, Saturating, Zero}, Perbill, Permill, }; pub use sp_std::vec::Vec; @@ -60,10 +60,13 @@ pub use pallet::*; mod test; mod types; -pub use types::*; // TODO: maybe make it more restrictive later +use types::*; +pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; const STAKING_ID: LockIdentifier = *b"dapstake"; +// TODO: add tracing! + #[frame_support::pallet] pub mod pallet { use super::*; @@ -1487,6 +1490,31 @@ pub mod pallet { Ok(()) } + + /// Used to enable or disable maintenance mode. + /// Can only be called by manager origin. + #[pallet::call_index(16)] + #[pallet::weight(Weight::zero())] + pub fn force(origin: OriginFor, force_type: ForcingType) -> DispatchResult { + // TODO: tests are missing but will be added later. + Self::ensure_pallet_enabled()?; + T::ManagerOrigin::ensure_origin(origin)?; + + // Ensure a 'change' happens on the next block + ActiveProtocolState::::mutate(|state| { + let current_block = frame_system::Pallet::::block_number(); + state.next_era_start = current_block.saturating_add(One::one()); + + match force_type { + ForcingType::Era => (), + ForcingType::PeriodType => { + state.period_info.ending_era = state.era.saturating_add(1); + } + } + }); + + Ok(()) + } } impl Pallet { @@ -1536,31 +1564,31 @@ pub mod pallet { } /// `true` if smart contract is active, `false` if it has been unregistered. - pub fn is_active(smart_contract: &T::SmartContract) -> bool { + pub(crate) fn is_active(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) } /// Calculates the `EraRewardSpan` index for the specified era. - pub fn era_reward_span_index(era: EraNumber) -> EraNumber { + pub(crate) fn era_reward_span_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } /// Return the oldest period for which rewards can be claimed. /// All rewards before that period are considered to be expired. - pub fn oldest_claimable_period(current_period: PeriodNumber) -> PeriodNumber { + pub(crate) fn oldest_claimable_period(current_period: PeriodNumber) -> PeriodNumber { current_period.saturating_sub(T::RewardRetentionInPeriods::get()) } /// Unlocking period expressed in the number of blocks. - pub fn unlock_period() -> BlockNumberFor { + pub(crate) fn unlock_period() -> BlockNumberFor { T::StandardEraLength::get().saturating_mul(T::UnlockingPeriod::get().into()) } /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. /// /// The returned object contains information about each dApp that made it into a tier. - pub fn get_dapp_tier_assignment( + pub(crate) fn get_dapp_tier_assignment( era: EraNumber, period: PeriodNumber, dapp_reward_pool: Balance, diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index a823fc1abc..9120b318f6 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -19,8 +19,8 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ - pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, IntegratedDApps, - Ledger, NextDAppId, PeriodNumber, PeriodType, + pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, + IntegratedDApps, Ledger, NextDAppId, PeriodNumber, PeriodType, }; use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; @@ -129,6 +129,10 @@ fn maintenace_mode_call_filtering_works() { DappStaking::cleanup_expired_entries(RuntimeOrigin::signed(1)), Error::::Disabled ); + assert_noop!( + DappStaking::force(RuntimeOrigin::root(), ForcingType::Era), + Error::::Disabled + ); }) } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 08320b29e6..3f5f6a7cb1 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -141,11 +141,11 @@ pub struct PeriodEndInfo { /// Force types to speed up the next era, and even period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub enum ForcingTypes { +pub enum ForcingType { /// Force the next era to start. - NewEra, - /// Force the current period phase to end, and new one to start - NewEraAndPeriodPhase, + Era, + /// Force the current period type to end, and new one to start. It will also force a new era to start. + PeriodType, } /// General information & state of the dApp staking protocol. @@ -1012,6 +1012,10 @@ impl SingularStakingInfo { } } +// TODO: Current implementation doesn't require off-chain worker so we don't need 3 entries - only 2 are enough. +// This means that implementation can be simplified to work similar as `AccountLedger` does, where current and future entry exist. +// Even in case reward calculation requires multiple blocks to finish, we could simply mark all stake calls as invalid during this short period. + const STAKING_SERIES_HISTORY: u32 = 3; /// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. From 1e80e092d6857cfa7b35ff77de667b148fb33184 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 25 Oct 2023 17:23:49 +0200 Subject: [PATCH 29/86] try runtime build fix --- pallets/dapp-staking-v3/Cargo.toml | 1 + runtime/local/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index acc457ac8f..3d33ab208a 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -43,3 +43,4 @@ std = [ "pallet-balances/std", "astar-primitives/std", ] +try-runtime = ["frame-support/try-runtime"] diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index a1280f19d6..dbb5d739f4 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -203,6 +203,7 @@ try-runtime = [ "pallet-contracts/try-runtime", "pallet-custom-signatures/try-runtime", "pallet-dapps-staking/try-runtime", + "pallet-dapp-staking-v3/try-runtime", "pallet-grandpa/try-runtime", "pallet-insecure-randomness-collective-flip/try-runtime", "pallet-sudo/try-runtime", From fbe4c4d7398aa5aca94e2de542ee9d9771e24300 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 26 Oct 2023 17:20:31 +0200 Subject: [PATCH 30/86] Minor changes --- pallets/dapp-staking-v3/src/lib.rs | 6 ++++-- .../dapp-staking-v3/src/test/testing_utils.rs | 4 ++-- pallets/dapp-staking-v3/src/test/tests.rs | 18 +++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 7732d0c61a..8e5be86c42 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -478,6 +478,8 @@ pub mod pallet { fn on_initialize(now: BlockNumberFor) -> Weight { let mut protocol_state = ActiveProtocolState::::get(); + // TODO: maybe do lazy history cleanup in this function? + // We should not modify pallet storage while in maintenance mode. // This is a safety measure, since maintenance mode is expected to be // enabled in case some misbehavior or corrupted storage is detected. @@ -671,13 +673,13 @@ pub mod pallet { Ok(()) } - /// Used to modify the reward destination account for a dApp. + /// Used to modify the reward beneficiary account for a dApp. /// /// Caller has to be dApp owner. /// If set to `None`, rewards will be deposited to the dApp owner. #[pallet::call_index(2)] #[pallet::weight(Weight::zero())] - pub fn set_dapp_reward_destination( + pub fn set_dapp_reward_beneficiary( origin: OriginFor, smart_contract: T::SmartContract, beneficiary: Option, diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index c3c06f8534..4139fe72c2 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -114,13 +114,13 @@ pub(crate) fn assert_register(owner: AccountId, smart_contract: &MockSmartContra } /// Update dApp reward destination and assert success -pub(crate) fn assert_set_dapp_reward_destination( +pub(crate) fn assert_set_dapp_reward_beneficiary( owner: AccountId, smart_contract: &MockSmartContract, beneficiary: Option, ) { // Change reward destination - assert_ok!(DappStaking::set_dapp_reward_destination( + assert_ok!(DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(owner), smart_contract.clone(), beneficiary, diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 9120b318f6..54ebd244fa 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -63,7 +63,7 @@ fn maintenace_mode_call_filtering_works() { Error::::Disabled ); assert_noop!( - DappStaking::set_dapp_reward_destination( + DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(1), MockSmartContract::Wasm(1), Some(2) @@ -270,7 +270,7 @@ fn register_past_sentinel_value_of_id_fails() { } #[test] -fn set_dapp_reward_destination_for_contract_is_ok() { +fn set_dapp_reward_beneficiary_for_contract_is_ok() { ExtBuilder::build().execute_with(|| { // Prepare & register smart contract let owner = 1; @@ -282,21 +282,21 @@ fn set_dapp_reward_destination_for_contract_is_ok() { .unwrap() .reward_destination .is_none()); - assert_set_dapp_reward_destination(owner, &smart_contract, Some(3)); - assert_set_dapp_reward_destination(owner, &smart_contract, Some(5)); - assert_set_dapp_reward_destination(owner, &smart_contract, None); + assert_set_dapp_reward_beneficiary(owner, &smart_contract, Some(3)); + assert_set_dapp_reward_beneficiary(owner, &smart_contract, Some(5)); + assert_set_dapp_reward_beneficiary(owner, &smart_contract, None); }) } #[test] -fn set_dapp_reward_destination_fails() { +fn set_dapp_reward_beneficiary_fails() { ExtBuilder::build().execute_with(|| { let owner = 1; let smart_contract = MockSmartContract::Wasm(3); // Contract doesn't exist yet assert_noop!( - DappStaking::set_dapp_reward_destination( + DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(owner), smart_contract, Some(5) @@ -307,7 +307,7 @@ fn set_dapp_reward_destination_fails() { // Non-owner cannnot change reward destination assert_register(owner, &smart_contract); assert_noop!( - DappStaking::set_dapp_reward_destination( + DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(owner + 1), smart_contract, Some(5) @@ -1479,7 +1479,7 @@ fn claim_dapp_reward_works() { // Advance to next era, and ensure rewards can be paid out to a custom beneficiary let new_beneficiary = 17; - assert_set_dapp_reward_destination(dev_account, &smart_contract, Some(new_beneficiary)); + assert_set_dapp_reward_beneficiary(dev_account, &smart_contract, Some(new_beneficiary)); advance_to_next_era(); assert_claim_dapp_reward( account, From 12d01fdf291dde0e79d47807204619b402df459b Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 27 Oct 2023 14:36:53 +0200 Subject: [PATCH 31/86] Minor --- pallets/dapp-staking-v3/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8e5be86c42..e953457aeb 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -989,6 +989,8 @@ pub mod pallet { let mut ledger = Ledger::::get(&account); + + // TODO: suggestion is to change this a bit so we clean up ledger if rewards have expired // 1. // Increase stake amount for the next era & current period in staker's ledger ledger @@ -1002,6 +1004,8 @@ pub mod pallet { _ => Error::::InternalStakeError, })?; + // TODO: also change this to check if rewards have expired + // 2. // Update `StakerInfo` storage with the new stake amount on the specified contract. // @@ -1456,6 +1460,7 @@ pub mod pallet { Ok(()) } + // TODO: an alternative to this could would be to allow `unstake` call to cleanup old entries, however that means more complexity in that call /// Used to unstake funds from a contract that was unregistered after an account staked on it. #[pallet::call_index(15)] #[pallet::weight(Weight::zero())] From d7255753077daa3570b763f29bc3c169a936d3b2 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 27 Oct 2023 15:21:54 +0200 Subject: [PATCH 32/86] Formatting --- pallets/dapp-staking-v3/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e953457aeb..8702c1ac88 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -989,7 +989,6 @@ pub mod pallet { let mut ledger = Ledger::::get(&account); - // TODO: suggestion is to change this a bit so we clean up ledger if rewards have expired // 1. // Increase stake amount for the next era & current period in staker's ledger From 72d39830d340a0390f56f4fd2b322ed42421f163 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 27 Oct 2023 16:15:59 +0200 Subject: [PATCH 33/86] Benchmarks INIT --- Cargo.lock | 1 + pallets/dapp-staking-v3/Cargo.toml | 9 +++++ pallets/dapp-staking-v3/src/benchmarking.rs | 45 +++++++++++++++++++++ pallets/dapp-staking-v3/src/lib.rs | 3 ++ runtime/local/Cargo.toml | 1 + 5 files changed, 59 insertions(+) create mode 100644 pallets/dapp-staking-v3/src/benchmarking.rs diff --git a/Cargo.lock b/Cargo.lock index ab6f066763..cf8bfcc37a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7113,6 +7113,7 @@ name = "pallet-dapp-staking-v3" version = "0.0.1-alpha" dependencies = [ "astar-primitives", + "frame-benchmarking", "frame-support", "frame-system", "num-traits", diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index 3d33ab208a..5b5dcd4c04 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -23,6 +23,8 @@ sp-std = { workspace = true } astar-primitives = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } + [dev-dependencies] pallet-balances = { workspace = true } @@ -42,5 +44,12 @@ std = [ "frame-system/std", "pallet-balances/std", "astar-primitives/std", + "frame-benchmarking/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs new file mode 100644 index 0000000000..bdc797bc75 --- /dev/null +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -0,0 +1,45 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::*; + +use frame_benchmarking::v2::*; +use frame_support::weights::Weight; + +use astar_primitives::Balance; + +#[benchmarks] +mod benchmarks { + use super::*; + + impl_benchmark_test_suite!( + Pallet, + crate::benchmarking::tests::new_test_ext(), + crate::mock::TestRuntime, + ); +} + +#[cfg(test)] +mod tests { + use crate::mock; + use sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + mock::ExtBuilder::default().build() + } +} diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8702c1ac88..4f3496585c 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -59,6 +59,9 @@ pub use pallet::*; #[cfg(test)] mod test; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + mod types; use types::*; pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index dbb5d739f4..3ecbb31994 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -189,6 +189,7 @@ runtime-benchmarks = [ "pallet-ethereum-checked/runtime-benchmarks", "astar-primitives/runtime-benchmarks", "pallet-assets/runtime-benchmarks", + "pallet-dapp-staking-v3/runtime-benchmarks", ] try-runtime = [ "fp-self-contained/try-runtime", From f0c14ccf76e1086f07dba2ee554ccf283b612267 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 27 Oct 2023 16:31:38 +0200 Subject: [PATCH 34/86] Compiling benchmarks --- pallets/dapp-staking-v3/Cargo.toml | 1 + pallets/dapp-staking-v3/src/benchmarking.rs | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index 5b5dcd4c04..0de398ce18 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -51,5 +51,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index bdc797bc75..e013a4ecf3 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -19,14 +19,30 @@ use super::*; use frame_benchmarking::v2::*; -use frame_support::weights::Weight; - use astar_primitives::Balance; #[benchmarks] mod benchmarks { use super::*; + #[benchmark] + fn dapp_tier_assignment() { + let era = 10; + let period = 1; + let reward_pool = Balance::from(1e30 as u128); + + TierConfig:::put() + // TODO: TierConfig setting + // TODO: dApp registration + // TODO: ContractStake filling + + + #[block] + { + let _ = Pallet::::get_dapp_tier_assignment(era, period, reward_pool); + } + } + impl_benchmark_test_suite!( Pallet, crate::benchmarking::tests::new_test_ext(), From e2447b334d87ef810b28c7ada8416e5420db070b Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 30 Oct 2023 08:05:40 +0100 Subject: [PATCH 35/86] Fix --- pallets/dapp-staking-v3/src/benchmarking.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index e013a4ecf3..820981c311 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -18,8 +18,8 @@ use super::*; -use frame_benchmarking::v2::*; use astar_primitives::Balance; +use frame_benchmarking::v2::*; #[benchmarks] mod benchmarks { @@ -31,12 +31,10 @@ mod benchmarks { let period = 1; let reward_pool = Balance::from(1e30 as u128); - TierConfig:::put() // TODO: TierConfig setting // TODO: dApp registration // TODO: ContractStake filling - #[block] { let _ = Pallet::::get_dapp_tier_assignment(era, period, reward_pool); From 4b9e03d94b9c8cb8d41dffdaff994d1bd8346ea8 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 30 Oct 2023 11:20:05 +0100 Subject: [PATCH 36/86] dapp tier calculation benchmark --- bin/collator/src/local/chain_spec.rs | 18 +- pallets/dapp-staking-v3/src/benchmarking.rs | 199 ++++++++++++++++++-- pallets/dapp-staking-v3/src/lib.rs | 9 + pallets/dapp-staking-v3/src/test/mock.rs | 11 ++ pallets/dapp-staking-v3/src/test/mod.rs | 2 +- runtime/local/src/lib.rs | 14 +- 6 files changed, 230 insertions(+), 23 deletions(-) diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index 4774784754..1cde2e4dd7 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -22,7 +22,7 @@ use local_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, BalancesConfig, BaseFeeConfig, BlockRewardConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMConfig, GenesisConfig, GrandpaConfig, GrandpaId, Precompiles, Signature, SudoConfig, SystemConfig, - TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, + TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, AST, }; use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; @@ -114,7 +114,7 @@ fn testnet_genesis( balances: endowed_accounts .iter() .cloned() - .map(|k| (k, 1_000_000_000_000_000_000_000_000_000)) + .map(|k| (k, 1_000_000_000 * AST)) .collect(), }, block_reward: BlockRewardConfig { @@ -198,18 +198,18 @@ fn testnet_genesis( ], tier_thresholds: vec![ TierThreshold::DynamicTvlAmount { - amount: 100, - minimum_amount: 80, + amount: 100 * AST, + minimum_amount: 80 * AST, }, TierThreshold::DynamicTvlAmount { - amount: 50, - minimum_amount: 40, + amount: 50 * AST, + minimum_amount: 40 * AST, }, TierThreshold::DynamicTvlAmount { - amount: 20, - minimum_amount: 20, + amount: 20 * AST, + minimum_amount: 20 * AST, }, - TierThreshold::FixedTvlAmount { amount: 10 }, + TierThreshold::FixedTvlAmount { amount: 10 * AST }, ], slots_per_tier: vec![10, 20, 30, 40], }, diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 820981c311..2ce41bef78 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -16,44 +16,219 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use super::*; +use super::{Pallet as DappStaking, *}; use astar_primitives::Balance; use frame_benchmarking::v2::*; +use frame_support::assert_ok; +use frame_system::{Pallet as System, RawOrigin}; + +// TODO: copy/paste from mock, make it more generic later + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +fn run_to_block(n: BlockNumberFor) { + while System::::block_number() < n { + DappStaking::::on_finalize(System::::block_number()); + System::::set_block_number(System::::block_number() + One::one()); + // This is performed outside of dapps staking but we expect it before on_initialize + DappStaking::::on_initialize(System::::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +fn run_for_blocks(n: BlockNumberFor) { + run_to_block::(System::::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(crate) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next era has been reached. +pub(crate) fn advance_to_next_era() { + advance_to_era::(ActiveProtocolState::::get().era + 1); +} + +// /// Advance blocks until the specified period has been reached. +// /// +// /// Function has no effect if period is already passed. +// pub(crate) fn advance_to_period(period: PeriodNumber) { +// assert!(period >= ActiveProtocolState::::get().period_number()); +// while ActiveProtocolState::::get().period_number() < period { +// run_for_blocks::(One::one()); +// } +// } + +// /// Advance blocks until next period has been reached. +// pub(crate) fn advance_to_next_period() { +// advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +// } + +// /// Advance blocks until next period type has been reached. +// pub(crate) fn advance_to_next_period_type() { +// let period_type = ActiveProtocolState::::get().period_type(); +// while ActiveProtocolState::::get().period_type() == period_type { +// run_for_blocks::(One::one()); +// } +// } + +// All our networks use 18 decimals for native currency so this should be fine. +const UNIT: Balance = 1_000_000_000_000_000_000; + +// Minimum amount that must be staked on a dApp to enter any tier +const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; + +const NUMBER_OF_SLOTS: u16 = 100; + +pub fn initial_config() { + let era_length = T::StandardEraLength::get(); + let voting_period_length_in_eras = T::StandardErasPerVotingPeriod::get(); + + // Init protocol state + ActiveProtocolState::::put(ProtocolState { + era: 1, + next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + One::one(), + period_info: PeriodInfo { + number: 1, + period_type: PeriodType::Voting, + ending_era: 2, + }, + maintenance: false, + }); + + // Init tier params + let tier_params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 100 * UNIT, + minimum_amount: 80 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 50 * UNIT, + minimum_amount: 40 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 20 * UNIT, + minimum_amount: 20 * UNIT, + }, + TierThreshold::FixedTvlAmount { + amount: MIN_TIER_THRESHOLD, + }, + ]) + .unwrap(), + }; + + // Init tier config, based on the initial params + let init_tier_config = TiersConfiguration:: { + number_of_slots: NUMBER_OF_SLOTS, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + + assert!(tier_params.is_valid()); + assert!(init_tier_config.is_valid()); + + StaticTierParams::::put(tier_params); + TierConfig::::put(init_tier_config.clone()); + NextTierConfig::::put(init_tier_config); +} + +fn max_number_of_contracts() -> u32 { + T::MaxNumberOfContracts::get().min(NUMBER_OF_SLOTS).into() +} + #[benchmarks] mod benchmarks { use super::*; #[benchmark] - fn dapp_tier_assignment() { - let era = 10; - let period = 1; - let reward_pool = Balance::from(1e30 as u128); + fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + let developer: T::AccountId = whitelisted_caller(); + for id in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + developer.clone().into(), + smart_contract, + )); + } + + let mut amount = MIN_TIER_THRESHOLD; + for id in 0..x { + let staker = account("staker", id.into(), 1337); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + amount, + )); + + // Slowly increase the stake amount + amount.saturating_accrue(UNIT); + } + + // Advance to next era + advance_to_next_era::(); - // TODO: TierConfig setting - // TODO: dApp registration - // TODO: ContractStake filling + let reward_era = ActiveProtocolState::::get().era; + let reward_period = ActiveProtocolState::::get().period_number(); + let reward_pool = Balance::from(10_000 * UNIT as u128); #[block] { - let _ = Pallet::::get_dapp_tier_assignment(era, period, reward_pool); + let dapp_tiers = + Pallet::::get_dapp_tier_assignment(reward_era, reward_period, reward_pool); + // TODO: how to move this outside of the 'block'? Cannot declare it outside, and then use it inside. + assert_eq!(dapp_tiers.dapps.len(), x as usize); } } impl_benchmark_test_suite!( Pallet, crate::benchmarking::tests::new_test_ext(), - crate::mock::TestRuntime, + crate::test::mock::Test, ); } #[cfg(test)] mod tests { - use crate::mock; + use crate::test::mock; use sp_io::TestExternalities; pub fn new_test_ext() -> TestExternalities { - mock::ExtBuilder::default().build() + mock::ExtBuilder::build() } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 4f3496585c..5aec2a960d 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -81,6 +81,11 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + #[cfg(feature = "runtime-benchmarks")] + pub trait BenchmarkHelper { + fn get_smart_contract(id: u32) -> SmartContract; + } + #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. @@ -158,6 +163,10 @@ pub mod pallet { /// Number of different tiers. #[pallet::constant] type NumberOfTiers: Get; + + /// Helper trait for benchmarks. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; } #[pallet::event] diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 6b05742d93..3c4df76d9c 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -138,6 +138,15 @@ impl Default for MockSmartContract { } } +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData); +#[cfg(feature = "runtime-benchmarks")] +impl crate::BenchmarkHelper for BenchmarkHelper { + fn get_smart_contract(id: u32) -> MockSmartContract { + MockSmartContract::Wasm(id as AccountId) + } +} + impl pallet_dapp_staking::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -157,6 +166,8 @@ impl pallet_dapp_staking::Config for Test { type MaxNumberOfStakedContracts = ConstU32<3>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper; } pub struct ExtBuilder; diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs index 94a090243c..0774935213 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -mod mock; +pub(crate) mod mock; mod testing_utils; mod tests; mod tests_types; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index addebe80e1..6c18ef4e02 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -459,6 +459,16 @@ impl pallet_dapp_staking_v3::RewardPoolProvider for DummyRewardPoolProvider { Balance::from(3_000_000 * AST) } } +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData); +#[cfg(feature = "runtime-benchmarks")] +impl pallet_dapp_staking_v3::BenchmarkHelper> + for BenchmarkHelper> +{ + fn get_smart_contract(id: u32) -> SmartContract { + SmartContract::Wasm(AccountId::from([id as u8; 32])) + } +} impl pallet_dapp_staking_v3::Config for Runtime { type RuntimeEvent = RuntimeEvent; @@ -472,13 +482,15 @@ impl pallet_dapp_staking_v3::Config for Runtime { type StandardErasPerBuildAndEarnPeriod = ConstU32<10>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<10>; + type MaxNumberOfContracts = ConstU16<100>; type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU32<2>; type MaxNumberOfStakedContracts = ConstU32<3>; type MinimumStakeAmount = ConstU128; type NumberOfTiers = ConstU32<4>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper>; } impl pallet_utility::Config for Runtime { From 8982e100df570871de4bb213793398bb67a50cb1 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 30 Oct 2023 12:03:54 +0100 Subject: [PATCH 37/86] Measured tier assignment --- pallets/dapp-staking-v3/src/dsv3_weight.rs | 104 +++++++++++++++++++++ pallets/dapp-staking-v3/src/lib.rs | 2 + pallets/dapp-staking-v3/src/test/tests.rs | 11 +++ runtime/local/src/lib.rs | 2 + 4 files changed, 119 insertions(+) create mode 100644 pallets/dapp-staking-v3/src/dsv3_weight.rs diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs new file mode 100644 index 0000000000..64948127b5 --- /dev/null +++ b/pallets/dapp-staking-v3/src/dsv3_weight.rs @@ -0,0 +1,104 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_v3 +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-10-30, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Dinos-MBP`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_v3 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=dsv3_weight.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_dapp_staking_v3. +pub trait WeightInfo { + fn dapp_tier_assignment(x: u32, ) -> Weight; +} + +/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:101 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:100 w:0) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(170), added: 2645, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `823 + x * (164 ±0)` + // Estimated: `3586 + x * (2645 ±0)` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(12_342_575, 3586) + // Standard Error: 16_840 + .saturating_add(Weight::from_parts(7_051_078, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2645).saturating_mul(x.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:101 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:100 w:0) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(170), added: 2645, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `823 + x * (164 ±0)` + // Estimated: `3586 + x * (2645 ±0)` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(12_342_575, 3586) + // Standard Error: 16_840 + .saturating_add(Weight::from_parts(7_051_078, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2645).saturating_mul(x.into())) + } +} diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 5aec2a960d..e6abcfbd19 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -66,6 +66,8 @@ mod types; use types::*; pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; +mod dsv3_weight; + const STAKING_ID: LockIdentifier = *b"dapstake"; // TODO: add tracing! diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 54ebd244fa..f870956a96 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -29,6 +29,17 @@ use sp_runtime::traits::Zero; // TODO: test scenarios // 1. user is staking, period passes, they can unlock their funds which were previously staked +#[test] +fn print_test() { + ExtBuilder::build().execute_with(|| { + use crate::dsv3_weight::WeightInfo; + println!( + ">>> {:?}", + crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(200) + ); + }) +} + #[test] fn maintenace_mode_works() { ExtBuilder::build().execute_with(|| { diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 6c18ef4e02..12ec1e4062 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -459,6 +459,7 @@ impl pallet_dapp_staking_v3::RewardPoolProvider for DummyRewardPoolProvider { Balance::from(3_000_000 * AST) } } + #[cfg(feature = "runtime-benchmarks")] pub struct BenchmarkHelper(sp_std::marker::PhantomData); #[cfg(feature = "runtime-benchmarks")] @@ -1192,6 +1193,7 @@ mod benches { [pallet_dapps_staking, DappsStaking] [pallet_block_reward, BlockReward] [pallet_ethereum_checked, EthereumChecked] + [pallet_dapp_staking_v3, DappStaking] ); } From ee50c2d5d62f3253587bb5d72bf623cc66e76bdf Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 30 Oct 2023 16:01:48 +0100 Subject: [PATCH 38/86] Decending rewards in benchmarks --- pallets/dapp-staking-v3/src/benchmarking.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 2ce41bef78..008ca81c00 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -180,7 +180,8 @@ mod benchmarks { )); } - let mut amount = MIN_TIER_THRESHOLD; + // TODO: try to make this more "shuffled" so the generated vector ends up being more random + let mut amount = 1000 * MIN_TIER_THRESHOLD; for id in 0..x { let staker = account("staker", id.into(), 1337); T::Currency::make_free_balance_be(&staker, amount); @@ -196,8 +197,8 @@ mod benchmarks { amount, )); - // Slowly increase the stake amount - amount.saturating_accrue(UNIT); + // Slowly decrease the stake amount + amount.saturating_dec(UNIT); } // Advance to next era From 67de44c9b3b3e35047b9f14a69ce08310c483772 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 31 Oct 2023 08:31:40 +0100 Subject: [PATCH 39/86] Series refactoring & partial tests --- pallets/dapp-staking-v3/src/benchmarking.rs | 2 +- pallets/dapp-staking-v3/src/lib.rs | 14 +- .../dapp-staking-v3/src/test/tests_types.rs | 223 +++----------- pallets/dapp-staking-v3/src/types.rs | 274 +++++++----------- 4 files changed, 134 insertions(+), 379 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 008ca81c00..5f71f00ee3 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -198,7 +198,7 @@ mod benchmarks { )); // Slowly decrease the stake amount - amount.saturating_dec(UNIT); + amount.saturating_reduce(UNIT); } // Advance to next era diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e6abcfbd19..a4edd11cb6 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1067,12 +1067,7 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the new stake amount on the specified contract. let mut contract_stake_info = ContractStake::::get(&smart_contract); - ensure!( - contract_stake_info - .stake(amount, protocol_state.period_info, stake_era) - .is_ok(), - Error::::InternalStakeError - ); + contract_stake_info.stake(amount, protocol_state.period_info, stake_era); // 4. // Update total staked amount for the next era. @@ -1170,12 +1165,7 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the reduced stake amount on the specified contract. let mut contract_stake_info = ContractStake::::get(&smart_contract); - ensure!( - contract_stake_info - .unstake(amount, protocol_state.period_info, unstake_era) - .is_ok(), - Error::::InternalUnstakeError - ); + contract_stake_info.unstake(amount, protocol_state.period_info, unstake_era); // 4. // Update total staked amount for the next era. diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 1211792180..93d8b5896c 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1141,18 +1141,18 @@ fn singular_staking_info_unstake_during_bep_is_ok() { fn contract_stake_amount_info_series_get_works() { let info_1 = StakeAmount::new(0, 0, 4, 2); let info_2 = StakeAmount::new(11, 0, 7, 3); - let info_3 = StakeAmount::new(0, 13, 9, 3); - let series = ContractStakeAmountSeries::new(vec![info_1, info_2, info_3]); + let series = ContractStakeAmountSeries { + staked: info_1, + staked_future: Some(info_2), + }; // Sanity check - assert_eq!(series.len(), 3); assert!(!series.is_empty()); // 1st scenario - get existing entries assert_eq!(series.get(4, 2), Some(info_1)); assert_eq!(series.get(7, 3), Some(info_2)); - assert_eq!(series.get(9, 3), Some(info_3)); // 2nd scenario - get non-existing entries for covered eras { @@ -1180,19 +1180,13 @@ fn contract_stake_amount_info_series_get_works() { fn contract_stake_amount_info_series_stake_is_ok() { let mut series = ContractStakeAmountSeries::default(); - // Sanity check - assert!(series.is_empty()); - assert!(series.len().is_zero()); - // 1st scenario - stake some amount and verify state change let era_1 = 3; let stake_era_1 = era_1 + 1; let period_1 = 5; let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); let amount_1 = 31; - assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); - - assert_eq!(series.len(), 1); + series.stake(amount_1, period_info_1, era_1); assert!(!series.is_empty()); assert!( @@ -1208,12 +1202,7 @@ fn contract_stake_amount_info_series_stake_is_ok() { // 2nd scenario - stake some more to the same era but different period type, and verify state change. let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); - assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); - assert_eq!( - series.len(), - 1, - "No new entry should be created since it's the same era." - ); + series.stake(amount_1, period_info_1, era_1); let entry_1_2 = series.get(stake_era_1, period_1).unwrap(); assert_eq!(entry_1_2.era, stake_era_1); assert_eq!(entry_1_2.total(), amount_1 * 2); @@ -1222,8 +1211,7 @@ fn contract_stake_amount_info_series_stake_is_ok() { let era_2 = era_1 + 2; let stake_era_2 = era_2 + 1; let amount_2 = 37; - assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); - assert_eq!(series.len(), 2); + series.stake(amount_2, period_info_1, era_2); let entry_2_1 = series.get(stake_era_1, period_1).unwrap(); let entry_2_2 = series.get(stake_era_2, period_1).unwrap(); assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); @@ -1242,61 +1230,35 @@ fn contract_stake_amount_info_series_stake_is_ok() { let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); let amount_3 = 41; - assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); - assert_eq!(series.len(), 3); - let entry_3_1 = series.get(stake_era_1, period_1).unwrap(); - let entry_3_2 = series.get(stake_era_2, period_1).unwrap(); - let entry_3_3 = series.get(stake_era_3, period_2).unwrap(); - assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); - assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); - assert_eq!(entry_3_3.era, stake_era_3); - assert_eq!(entry_3_3.period, period_2); - assert_eq!( - entry_3_3.total(), + series.stake(amount_3, period_info_2, era_3); + assert!( + series.get(stake_era_1, period_1).is_none(), + "Old period must be removed." + ); + assert!( + series.get(stake_era_2, period_1).is_none(), + "Old period must be removed." + ); + let entry_3_1 = series.get(stake_era_3, period_2).unwrap(); + assert_eq!(entry_3_1.era, stake_era_3); + assert_eq!(entry_3_1.period, period_2); + assert_eq!( + entry_3_1.total(), amount_3, "No carry over from previous entry since period has changed." ); - // 5th scenario - stake to the next era, expect cleanup of oldest entry + // 5th scenario - stake to the next era let era_4 = era_3 + 1; let stake_era_4 = era_4 + 1; let amount_4 = 5; - assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); - assert_eq!(series.len(), 3); - let entry_4_1 = series.get(stake_era_2, period_1).unwrap(); - let entry_4_2 = series.get(stake_era_3, period_2).unwrap(); - let entry_4_3 = series.get(stake_era_4, period_2).unwrap(); - assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); - assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); - assert_eq!(entry_4_3.era, stake_era_4); - assert_eq!(entry_4_3.period, period_2); - assert_eq!(entry_4_3.total(), amount_3 + amount_4); -} - -#[test] -fn contract_stake_amount_info_series_stake_with_inconsistent_data_fails() { - let mut series = ContractStakeAmountSeries::default(); - - // Create an entry with some staked amount - let era = 5; - let period_info = PeriodInfo { - number: 7, - period_type: PeriodType::Voting, - ending_era: 31, - }; - let amount = 37; - assert!(series.stake(amount, period_info, era).is_ok()); - - // 1st scenario - attempt to stake using old era - assert!(series.stake(amount, period_info, era - 1).is_err()); - - // 2nd scenario - attempt to stake using old period - let period_info = PeriodInfo { - number: period_info.number - 1, - period_type: PeriodType::Voting, - ending_era: 31, - }; - assert!(series.stake(amount, period_info, era).is_err()); + series.stake(amount_4, period_info_2, era_4); + let entry_4_1 = series.get(stake_era_3, period_2).unwrap(); + let entry_4_2 = series.get(stake_era_4, period_2).unwrap(); + assert_eq!(entry_4_1, entry_3_1, "Old entry must remain unchanged."); + assert_eq!(entry_4_2.era, stake_era_4); + assert_eq!(entry_4_2.period, period_2); + assert_eq!(entry_4_2.total(), amount_3 + amount_4); } #[test] @@ -1305,29 +1267,25 @@ fn contract_stake_amount_info_series_unstake_is_ok() { // Prep action - create a stake entry let era_1 = 2; - let stake_era_1 = era_1 + 1; let period = 3; let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); let stake_amount = 100; - assert!(series.stake(stake_amount, period_info, era_1).is_ok()); + series.stake(stake_amount, period_info, era_1); // 1st scenario - unstake in the same era let amount_1 = 5; - assert!(series.unstake(amount_1, period_info, era_1).is_ok()); - assert_eq!(series.len(), 1); + series.unstake(amount_1, period_info, era_1); assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); assert_eq!( series.staked_amount(period, PeriodType::Voting), stake_amount - amount_1 ); - // 2nd scenario - unstake in the future era, creating a 'gap' in the series - // [(era: 2)] ---> [(era: 2), (era: 5)] + // 2nd scenario - unstake in the future era, entries should be aligned to the current era let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); let era_2 = era_1 + 3; let amount_2 = 7; - assert!(series.unstake(amount_2, period_info, era_2).is_ok()); - assert_eq!(series.len(), 2); + series.unstake(amount_2, period_info, era_2); assert_eq!( series.total_staked_amount(period), stake_amount - amount_1 - amount_2 @@ -1336,121 +1294,6 @@ fn contract_stake_amount_info_series_unstake_is_ok() { series.staked_amount(period, PeriodType::Voting), stake_amount - amount_1 - amount_2 ); - - // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones - // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] - let era_3 = era_2 - 1; - let amount_3 = 11; - assert!(series.unstake(amount_3, period_info, era_3).is_ok()); - assert_eq!(series.len(), 3); - assert_eq!( - series.total_staked_amount(period), - stake_amount - amount_1 - amount_2 - amount_3 - ); - assert_eq!( - series.staked_amount(period, PeriodType::Voting), - stake_amount - amount_1 - amount_2 - amount_3 - ); - - // Check concrete entries - assert_eq!( - series.get(stake_era_1, period).unwrap().total(), - stake_amount - amount_1, - "Oldest entry must remain unchanged." - ); - assert_eq!( - series.get(era_2, period).unwrap().total(), - stake_amount - amount_1 - amount_2 - amount_3, - "Future era entry must be updated with all of the reductions." - ); - assert_eq!( - series.get(era_3, period).unwrap().total(), - stake_amount - amount_1 - amount_3, - "Second to last era entry must be updated with first & last reduction\ - because it derives its initial value from the oldest entry." - ); -} - -#[test] -fn contract_stake_amount_info_unstake_with_worst_case_scenario_for_capacity_overflow() { - let (era_1, era_2, era_3) = (4, 7, 9); - let (period_1, period_2) = (2, 3); - let (stake_amount_2, stake_amount_3) = (11, 13); - let info_1 = StakeAmount::new(11, 0, era_1, period_1); - let info_2 = StakeAmount::new(stake_amount_2, 0, era_2, period_2); - let info_3 = StakeAmount::new(0, stake_amount_3, era_3, period_2); - - // A gap between 2nd and 3rd era, and from that gap unstake will be done. - // This will force a new entry to be created, potentially overflowing the vector capacity. - let mut series = ContractStakeAmountSeries::new(vec![info_1, info_2, info_3]); - - // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity - let period_info = PeriodInfo { - number: period_2, - period_type: PeriodType::BuildAndEarn, - ending_era: 51, - }; - let unstake_amount = 3; - assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); - assert_eq!(series.len(), 3); - - assert_eq!( - series.get(era_1, period_1), - None, - "Oldest entry should have been prunned" - ); - assert_eq!( - series - .get(era_2, period_2) - .expect("Entry must exist.") - .total(), - stake_amount_2 - ); - assert_eq!( - series - .get(era_2 + 1, period_2) - .expect("Entry must exist.") - .total(), - stake_amount_2 - unstake_amount - ); - assert_eq!( - series - .get(era_3, period_2) - .expect("Entry must exist.") - .total(), - stake_amount_3 - unstake_amount - ); -} - -#[test] -fn contract_stake_amount_info_series_unstake_with_inconsistent_data_fails() { - let mut series = ContractStakeAmountSeries::default(); - let era = 5; - let stake_era = era + 1; - let period = 2; - let period_info = PeriodInfo { - number: period, - period_type: PeriodType::Voting, - ending_era: 31, - }; - - // 1st - Unstake from empty series - assert!(series.unstake(1, period_info, era).is_err()); - - // 2nd - Unstake with old period - let amount = 37; - assert!(series.stake(amount, period_info, era).is_ok()); - - let old_period_info = { - let mut temp = period_info.clone(); - temp.number -= 1; - temp - }; - assert!(series.unstake(1, old_period_info, stake_era).is_err()); - - // 3rd - Unstake with 'too' old era - assert!(series.unstake(1, period_info, stake_era - 2).is_err()); - assert!(series.unstake(1, period_info, stake_era - 1).is_ok()); } #[test] diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 3f5f6a7cb1..05366b23bb 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -1012,236 +1012,158 @@ impl SingularStakingInfo { } } -// TODO: Current implementation doesn't require off-chain worker so we don't need 3 entries - only 2 are enough. -// This means that implementation can be simplified to work similar as `AccountLedger` does, where current and future entry exist. -// Even in case reward calculation requires multiple blocks to finish, we could simply mark all stake calls as invalid during this short period. - -const STAKING_SERIES_HISTORY: u32 = 3; - /// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. -#[derive( - Encode, - Decode, - MaxEncodedLen, - RuntimeDebugNoBound, - PartialEqNoBound, - EqNoBound, - CloneNoBound, - TypeInfo, - Default, -)] -pub struct ContractStakeAmountSeries(BoundedVec>); +#[derive(Encode, Decode, MaxEncodedLen, RuntimeDebug, PartialEq, Eq, Clone, TypeInfo, Default)] +pub struct ContractStakeAmountSeries { + pub staked: StakeAmount, + pub staked_future: Option, +} impl ContractStakeAmountSeries { - /// Helper function to create a new instance of `ContractStakeAmountSeries`. - #[cfg(test)] - pub fn new(inner: Vec) -> Self { - Self(BoundedVec::try_from(inner).expect("Test should ensure this is always valid")) - } - - /// Returns inner `Vec` of `StakeAmount` instances. Useful for testing. - #[cfg(test)] - pub fn inner(&self) -> Vec { - self.0.clone().into_inner() - } - - /// Length of the series. - pub fn len(&self) -> usize { - self.0.len() - } - /// `true` if series is empty, `false` otherwise. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.staked.is_empty() && self.staked_future.is_none() } /// Latest period for which stake entry exists. pub fn latest_stake_period(&self) -> Option { - self.0.last().map(|x| x.period) + if let Some(stake_amount) = self.staked_future { + Some(stake_amount.period) + } else if !self.staked.is_empty() { + Some(self.staked.period) + } else { + None + } } /// Latest era for which stake entry exists. pub fn latest_stake_era(&self) -> Option { - self.0.last().map(|x| x.era) + if let Some(stake_amount) = self.staked_future { + Some(stake_amount.era) + } else if !self.staked.is_empty() { + Some(self.staked.era) + } else { + None + } } /// Returns the `StakeAmount` type for the specified era & period, if it exists. pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { - let idx = self - .0 - .binary_search_by(|stake_amount| stake_amount.era.cmp(&era)); - - // There are couple of distinct scenarios: - // 1. Era exists, so we just return it. - // 2. Era doesn't exist, and ideal index is zero, meaning there's nothing in history that would cover this era. - // 3. Era doesn't exist, and ideal index is greater than zero, meaning we can potentially use one of the previous entries to derive the information. - // 3.1. In case periods are matching, we return that value. - // 3.2. In case periods aren't matching, we return `None` since stakes don't carry over between periods. - match idx { - Ok(idx) => self.0.get(idx).map(|x| *x), - Err(ideal_idx) => { - if ideal_idx.is_zero() { - None + let mut maybe_result = match (self.staked, self.staked_future) { + (_, Some(staked_future)) if staked_future.era <= era => { + if staked_future.period == period { + Some(staked_future) } else { - match self.0.get(ideal_idx - 1) { - Some(info) if info.period == period => { - let mut info = *info; - info.era = era; - Some(info) - } - _ => None, - } + None } } + (staked, _) if staked.era <= era && staked.period == period => Some(staked), + _ => None, + }; + + if let Some(result) = maybe_result.as_mut() { + result.era = era; } + + maybe_result } /// Total staked amount on the contract, in the active period. pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { - match self.0.last() { - Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(), + match (self.staked, self.staked_future) { + (_, Some(staked_future)) if staked_future.period == active_period => { + staked_future.total() + } + (staked, _) if staked.period == active_period => staked.total(), _ => Balance::zero(), } } /// Staked amount on the contract, for specified period type, in the active period. - pub fn staked_amount(&self, period: PeriodNumber, period_type: PeriodType) -> Balance { - match self.0.last() { - Some(stake_amount) if stake_amount.period == period => { - stake_amount.for_type(period_type) + pub fn staked_amount(&self, active_period: PeriodNumber, period_type: PeriodType) -> Balance { + match (self.staked, self.staked_future) { + (_, Some(staked_future)) if staked_future.period == active_period => { + staked_future.for_type(period_type) } + (staked, _) if staked.period == active_period => staked.for_type(period_type), _ => Balance::zero(), } } /// Stake the specified `amount` on the contract, for the specified `period_type` and `era`. - pub fn stake( - &mut self, - amount: Balance, - period_info: PeriodInfo, - current_era: EraNumber, - ) -> Result<(), ()> { + pub fn stake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { let stake_era = current_era.saturating_add(1); + // TODO: maybe keep the check that period/era aren't historical? + // TODO2: tests need to be re-writen for this after the refactoring - // Defensive check to ensure we don't end up in a corrupted state. Should never happen. - if let Some(stake_amount) = self.0.last() { - if stake_amount.era > stake_era || stake_amount.period > period_info.number { - return Err(()); + match self.staked_future.as_mut() { + // Future entry matches the era, just updated it and return + Some(stake_amount) if stake_amount.era == stake_era => { + stake_amount.add(amount, period_info.period_type); + return; + } + // Future entry has older era, but periods match so overwrite the 'current' entry with it + Some(stake_amount) if stake_amount.period == period_info.number => { + self.staked = *stake_amount; } + // Otherwise do nothing + _ => (), } - // Get the most relevant `StakeAmount` instance - let mut stake_amount = if let Some(stake_amount) = self.0.last() { - if stake_amount.era == stake_era { - // Era matches, so we just update the last element. - let stake_amount = *stake_amount; - let _ = self.0.pop(); - stake_amount - } else if stake_amount.period == period_info.number { - // Periods match so we should 'copy' the last element to get correct staking amount - let mut temp = *stake_amount; - temp.era = stake_era; - temp - } else { - // It's a new period, so we need a completely new instance - StakeAmount::new( - Balance::zero(), - Balance::zero(), - stake_era, - period_info.number, - ) - } - } else { - // It's a new period, so we need a completely new instance - StakeAmount::new( - Balance::zero(), - Balance::zero(), - stake_era, - period_info.number, - ) + // Prepare new entry + let mut new_entry = match self.staked { + // 'current' entry period matches so we use it as base for the new entry + stake_amount if stake_amount.period == period_info.number => stake_amount, + // otherwise just create a dummy new entry + _ => Default::default(), }; + new_entry.add(amount, period_info.period_type); + new_entry.era = stake_era; + new_entry.period = period_info.number; - // Update the stake amount - stake_amount.add(amount, period_info.period_type); + self.staked_future = Some(new_entry); - // This should be infalible due to previous checks that ensure we don't end up overflowing the vector. - self.prune(); - self.0.try_push(stake_amount).map_err(|_| ()) + // Convenience cleanup + if self.staked.period < period_info.number { + self.staked = Default::default(); + } } /// Unstake the specified `amount` from the contract, for the specified `period_type` and `era`. - pub fn unstake( - &mut self, - amount: Balance, - period_info: PeriodInfo, - era: EraNumber, - ) -> Result<(), ()> { - // TODO: look into refactoring/optimizing this - right now it's a bit complex. + pub fn unstake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { + // TODO: tests need to be re-writen for this after the refactoring - // Defensive check to ensure we don't end up in a corrupted state. Should never happen. - if let Some(stake_amount) = self.0.last() { - // It's possible last element refers to the upcoming era, hence the "-1" on the 'era'. - if stake_amount.era.saturating_sub(1) > era || stake_amount.period > period_info.number + // First align entries - we only need to keep track of the current era, and the next one + match self.staked_future { + // Future entry exists, but it covers current or older era. + Some(stake_amount) + if stake_amount.era <= current_era && stake_amount.period == period_info.number => { - return Err(()); + self.staked = stake_amount; + self.staked.era = current_era; + self.staked_future = None; } - } else { - // Vector is empty, should never happen. - return Err(()); + _ => (), } - // 1st step - remove the last element IFF it's for the next era. - // Unstake the requested amount from it. - let last_era_info = match self.0.last() { - Some(stake_amount) if stake_amount.era == era.saturating_add(1) => { - let mut stake_amount = *stake_amount; - stake_amount.subtract(amount, period_info.period_type); - let _ = self.0.pop(); - Some(stake_amount) - } - _ => None, - }; - - // 2nd step - 3 options: - // 1. - last element has a matching era so we just update it. - // 2. - last element has a past era and matching period, so we'll create a new entry based on it. - // 3. - last element has a past era and past period, meaning it's invalid. - let second_last_era_info = if let Some(stake_amount) = self.0.last_mut() { - if stake_amount.era == era { - stake_amount.subtract(amount, period_info.period_type); - None - } else if stake_amount.period == period_info.number { - let mut new_entry = *stake_amount; - new_entry.subtract(amount, period_info.period_type); - new_entry.era = era; - Some(new_entry) - } else { - None - } - } else { - None - }; - - // 3rd step - push the new entries, if they exist. - if let Some(info) = second_last_era_info { - self.prune(); - self.0.try_push(info).map_err(|_| ())?; - } - if let Some(info) = last_era_info { - self.prune(); - self.0.try_push(info).map_err(|_| ())?; + // Current entry is from the right period, but older era. Shift it to the current era. + if self.staked.era < current_era && self.staked.period == period_info.number { + self.staked.era = current_era; } - Ok(()) - } + // Subtract both amounts + self.staked.subtract(amount, period_info.period_type); + if let Some(stake_amount) = self.staked_future.as_mut() { + stake_amount.subtract(amount, period_info.period_type); + } - /// Used to remove past entries, in case vector is full. - fn prune(&mut self) { - // Prune the oldest entry if we have more than the limit - if self.0.len() == STAKING_SERIES_HISTORY as usize { - // This can be perhaps optimized so we prune entries which are very old. - // However, this makes the code more complex & more error prone. - // If kept like this, we always make sure we cover the history, and we never exceed it. - self.0.remove(0); + // Conevnience cleanup + if self.staked.is_empty() { + self.staked = Default::default(); + } + if let Some(stake_amount) = self.staked_future { + if stake_amount.is_empty() { + self.staked_future = None; + } } } } From c586a6cb6c1df9ab4f7b72102a5336e9c0bdcdf8 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 31 Oct 2023 17:17:43 +0100 Subject: [PATCH 40/86] Comments, minor changes --- pallets/dapp-staking-v3/src/lib.rs | 2 +- pallets/dapp-staking-v3/src/test/mod.rs | 4 ++-- pallets/dapp-staking-v3/src/test/testing_utils.rs | 2 +- pallets/dapp-staking-v3/src/test/tests_types.rs | 10 +++++----- pallets/dapp-staking-v3/src/types.rs | 11 ++++++----- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index a4edd11cb6..c601e4f178 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1381,7 +1381,7 @@ pub mod pallet { // Get reward destination, and deposit the reward. // TODO: should we check reward is greater than zero, or even more precise, it's greater than the existential deposit? Seems redundant but still... - let beneficiary = dapp_info.get_reward_beneficiary(); + let beneficiary = dapp_info.reward_beneficiary(); T::Currency::deposit_creating(beneficiary, amount); // Write back updated struct to prevent double reward claims diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs index 0774935213..6535a68447 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -17,6 +17,6 @@ // along with Astar. If not, see . pub(crate) mod mock; -mod testing_utils; -mod tests; +// mod testing_utils; +// mod tests; mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 4139fe72c2..f0508548a0 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -920,7 +920,7 @@ pub(crate) fn assert_claim_dapp_reward( ) { let pre_snapshot = MemorySnapshot::new(); let dapp_info = pre_snapshot.integrated_dapps.get(smart_contract).unwrap(); - let beneficiary = dapp_info.get_reward_beneficiary(); + let beneficiary = dapp_info.reward_beneficiary(); let pre_total_issuance = ::Currency::total_issuance(); let pre_free_balance = ::Currency::free_balance(beneficiary); diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 93d8b5896c..3b1ecd4cf5 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -16,11 +16,11 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . +use astar_primitives::{Balance, BlockNumber}; use frame_support::assert_ok; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::Permill; -use crate::test::mock::{Balance, *}; use crate::*; // Helper to generate custom `Get` types for testing the `AccountLedger` struct. @@ -109,7 +109,7 @@ fn protocol_state_basic_checks() { period_number, "Switching from 'Voting' to 'BuildAndEarn' should not trigger period bump." ); - assert_eq!(protocol_state.ending_era(), ending_era_1); + assert_eq!(protocol_state.period_end_era(), ending_era_1); assert!(!protocol_state.is_new_era(next_era_start_1 - 1)); assert!(protocol_state.is_new_era(next_era_start_1)); @@ -123,7 +123,7 @@ fn protocol_state_basic_checks() { period_number + 1, "Switching from 'BuildAndEarn' to 'Voting' must trigger period bump." ); - assert_eq!(protocol_state.ending_era(), ending_era_2); + assert_eq!(protocol_state.period_end_era(), ending_era_2); assert!(protocol_state.is_new_era(next_era_start_2)); } @@ -240,7 +240,7 @@ fn account_ledger_add_unlocking_chunk_works() { for i in 2..=UnlockingDummy::get() { let new_unlock_amount = unlock_amount + i as u128; assert!(acc_ledger - .add_unlocking_chunk(new_unlock_amount, block_number + i as u64) + .add_unlocking_chunk(new_unlock_amount, block_number + i) .is_ok()); total_unlocking += new_unlock_amount; assert_eq!(acc_ledger.unlocking_amount(), total_unlocking); @@ -253,7 +253,7 @@ fn account_ledger_add_unlocking_chunk_works() { // Any further addition should fail, resulting in a noop let acc_ledger_snapshot = acc_ledger.clone(); assert_eq!( - acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() as u64 + 1), + acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() + 1), Err(AccountLedgerError::NoCapacity) ); assert_eq!(acc_ledger, acc_ledger_snapshot); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 05366b23bb..b9bc521140 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -87,6 +87,7 @@ pub enum PeriodType { } impl PeriodType { + /// Next period type, after `self`. pub fn next(&self) -> Self { match self { PeriodType::Voting => PeriodType::BuildAndEarn, @@ -98,7 +99,7 @@ impl PeriodType { /// Info about the ongoing period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct PeriodInfo { - /// Period number. Increments after each build&earn period type. + /// Period number. #[codec(compact)] pub number: PeriodNumber, /// Subperiod type. @@ -196,7 +197,7 @@ where } /// Ending era of current period - pub fn ending_era(&self) -> EraNumber { + pub fn period_end_era(&self) -> EraNumber { self.period_info.ending_era } @@ -249,7 +250,7 @@ pub struct DAppInfo { impl DAppInfo { /// Reward destination account for this dApp. - pub fn get_reward_beneficiary(&self) -> &AccountId { + pub fn reward_beneficiary(&self) -> &AccountId { match &self.reward_destination { Some(account_id) => account_id, None => &self.owner, @@ -263,7 +264,7 @@ pub struct UnlockingChunk bool { self.voting.is_zero() && self.build_and_earn.is_zero() } From 5cf60a119310b16a5a29ae0e35c33fbbf9feacdf Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 31 Oct 2023 17:58:12 +0100 Subject: [PATCH 41/86] Tests, improvements --- pallets/dapp-staking-v3/src/lib.rs | 4 +- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- pallets/dapp-staking-v3/src/test/mod.rs | 4 +- pallets/dapp-staking-v3/src/test/tests.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 56 ++++++++++++--- pallets/dapp-staking-v3/src/types.rs | 70 ++++++++----------- 6 files changed, 83 insertions(+), 57 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index c601e4f178..2699e63bdf 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -523,7 +523,7 @@ pub mod pallet { next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.next_period_type(ending_era, build_and_earn_start_block); + protocol_state.into_next_period_type(ending_era, build_and_earn_start_block); era_info.migrate_to_next_era(Some(protocol_state.period_type())); @@ -575,7 +575,7 @@ pub mod pallet { let voting_period_length = Self::blocks_per_voting_period(); let next_era_start_block = now.saturating_add(voting_period_length); - protocol_state.next_period_type(ending_era, next_era_start_block); + protocol_state.into_next_period_type(ending_era, next_era_start_block); era_info.migrate_to_next_era(Some(protocol_state.period_type())); diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 3c4df76d9c..8dc7be2a08 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -304,7 +304,7 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_next_period_type() { +pub(crate) fn advance_to_into_next_period_type() { let period_type = ActiveProtocolState::::get().period_type(); while ActiveProtocolState::::get().period_type() == period_type { run_for_blocks(1); diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs index 6535a68447..0774935213 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -17,6 +17,6 @@ // along with Astar. If not, see . pub(crate) mod mock; -// mod testing_utils; -// mod tests; +mod testing_utils; +mod tests; mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index f870956a96..4b23f8c784 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1287,7 +1287,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_period( ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); - advance_to_next_period_type(); + advance_to_into_next_period_type(); advance_to_era(ActiveProtocolState::::get().period_info.ending_era - 1); assert_claim_staker_rewards(account); @@ -1412,7 +1412,7 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { assert_lock(account, lock_amount); // Stake in Build&Earn period type, advance to next era and try to claim bonus reward - advance_to_next_period_type(); + advance_to_into_next_period_type(); assert_eq!( ActiveProtocolState::::get().period_type(), PeriodType::BuildAndEarn, diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 3b1ecd4cf5..ca00d988b8 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -102,7 +102,7 @@ fn protocol_state_basic_checks() { // Toggle new period type check - 'Voting' to 'BuildAndEarn' let ending_era_1 = 23; let next_era_start_1 = 41; - protocol_state.next_period_type(ending_era_1, next_era_start_1); + protocol_state.into_next_period_type(ending_era_1, next_era_start_1); assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); assert_eq!( protocol_state.period_number(), @@ -116,7 +116,7 @@ fn protocol_state_basic_checks() { // Toggle from 'BuildAndEarn' over to 'Voting' let ending_era_2 = 24; let next_era_start_2 = 91; - protocol_state.next_period_type(ending_era_2, next_era_start_2); + protocol_state.into_next_period_type(ending_era_2, next_era_start_2); assert_eq!(protocol_state.period_type(), PeriodType::Voting); assert_eq!( protocol_state.period_number(), @@ -127,6 +127,26 @@ fn protocol_state_basic_checks() { assert!(protocol_state.is_new_era(next_era_start_2)); } +#[test] +fn dapp_info_basic_checks() { + let owner = 1; + let beneficiary = 3; + + let mut dapp_info = DAppInfo { + owner, + id: 7, + state: DAppState::Registered, + reward_destination: None, + }; + + // Owner receives reward in case no beneficiary is set + assert_eq!(*dapp_info.reward_beneficiary(), owner); + + // Beneficiary receives rewards in case it is set + dapp_info.reward_destination = Some(beneficiary); + assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); +} + #[test] fn account_ledger_default() { get_u32_type!(UnlockingDummy, 5); @@ -201,6 +221,11 @@ fn account_ledger_add_unlocking_chunk_works() { get_u32_type!(UnlockingDummy, 5); let mut acc_ledger = AccountLedger::::default(); + // Base sanity check + let default_unlocking_chunk = UnlockingChunk::::default(); + assert!(default_unlocking_chunk.amount.is_zero()); + assert!(default_unlocking_chunk.unlock_block.is_zero()); + // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop assert!(acc_ledger.add_unlocking_chunk(0, 0).is_ok()); @@ -336,7 +361,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { assert!(acc_ledger.staked.is_empty()); assert!(acc_ledger.staked_future.is_none()); - // 1st scenario - stake some amount, and ensure values are as expected. + // 1st scenario - stake some amount in Voting period, and ensure values are as expected. let first_era = 1; let period_1 = 1; let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); @@ -370,12 +395,21 @@ fn account_ledger_add_stake_amount_basic_example_works() { .staked_amount_for_type(PeriodType::BuildAndEarn, period_1) .is_zero()); - // Second scenario - stake some more to the same era + // Second scenario - stake some more, but to the next period type let snapshot = acc_ledger.staked; + let period_info_2 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); assert!(acc_ledger - .add_stake_amount(1, first_era, period_info_1) + .add_stake_amount(1, first_era, period_info_2) .is_ok()); assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), + stake_amount + ); + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::BuildAndEarn, period_1), + 1 + ); assert_eq!(acc_ledger.staked, snapshot); } @@ -634,15 +668,15 @@ fn account_ledger_unstake_from_invalid_era_fails() { .add_stake_amount(amount_1, era_1, period_info_1) .is_ok()); - // Try to add to the next era, it should fail. + // Try to unstake from the next era, it should fail. assert_eq!( - acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), + acc_ledger.unstake_amount(1, era_1 + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); - // Try to add to the next period, it should fail. + // Try to unstake from the next period, it should fail. assert_eq!( - acc_ledger.add_stake_amount( + acc_ledger.unstake_amount( 1, era_1, PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) @@ -655,11 +689,11 @@ fn account_ledger_unstake_from_invalid_era_fails() { acc_ledger.staked_future = None; assert_eq!( - acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), + acc_ledger.unstake_amount(1, era_1 + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); assert_eq!( - acc_ledger.add_stake_amount( + acc_ledger.unstake_amount( 1, era_1, PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index b9bc521140..9fda94b3a0 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -207,9 +207,8 @@ where self.next_era_start <= now } - // TODO: rename this into something better? /// Triggers the next period type, updating appropriate parameters. - pub fn next_period_type(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { + pub fn into_next_period_type(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { let period_number = if self.period_type() == PeriodType::BuildAndEarn { self.period_number().saturating_add(1) } else { @@ -480,6 +479,34 @@ where } } + /// Verify that current era and period info arguments are valid for `stake` and `unstake` operations. + fn verify_stake_unstake_args( + &self, + era: EraNumber, + current_period_info: &PeriodInfo, + ) -> Result<(), AccountLedgerError> { + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era + 1 { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + } + + Ok(()) + } + /// Adds the specified amount to total staked amount, if possible. /// /// Staking can only be done for the ongoing period, and era. @@ -501,24 +528,7 @@ where return Ok(()); } - if !self.staked.is_empty() { - // In case entry for the current era exists, it must match the era exactly. - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - // In case it doesn't (i.e. first time staking), then the future era must match exactly - // one era after the one provided via argument. - } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era + 1 { - return Err(AccountLedgerError::InvalidEra); - } - if stake_amount.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - } + self.verify_stake_unstake_args(era, ¤t_period_info)?; if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); @@ -556,25 +566,7 @@ where return Ok(()); } - // TODO: this is a duplicated check, maybe I should extract it into a function? - if !self.staked.is_empty() { - // In case entry for the current era exists, it must match the era exactly. - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - // In case it doesn't (i.e. first time staking), then the future era must match exactly - // one era after the one provided via argument. - } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era + 1 { - return Err(AccountLedgerError::InvalidEra); - } - if stake_amount.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - } + self.verify_stake_unstake_args(era, ¤t_period_info)?; // User must be precise with their unstake amount. if self.staked_amount(current_period_info.number) < amount { From 7a3c634160824249fb96a76d78d10d73f712c88f Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 2 Nov 2023 10:59:55 +0100 Subject: [PATCH 42/86] More tests, some minor refactoring --- pallets/dapp-staking-v3/src/lib.rs | 2 +- .../dapp-staking-v3/src/test/testing_utils.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 182 ++++++++++++++---- pallets/dapp-staking-v3/src/types.rs | 19 +- 4 files changed, 165 insertions(+), 42 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 2699e63bdf..060d691199 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -372,7 +372,7 @@ pub mod pallet { /// Information about how much has been staked on a smart contract in some era or period. #[pallet::storage] pub type ContractStake = - StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakeAmountSeries, ValueQuery>; + StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakeAmount, ValueQuery>; /// General information about the current era. #[pallet::storage] diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index f0508548a0..fbd90823d2 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -47,7 +47,7 @@ pub(crate) struct MemorySnapshot { ), SingularStakingInfo, >, - contract_stake: HashMap<::SmartContract, ContractStakeAmountSeries>, + contract_stake: HashMap<::SmartContract, ContractStakeAmount>, era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, dapp_tiers: HashMap>, @@ -432,7 +432,7 @@ pub(crate) fn assert_stake( let pre_contract_stake = pre_snapshot .contract_stake .get(&smart_contract) - .map_or(ContractStakeAmountSeries::default(), |series| { + .map_or(ContractStakeAmount::default(), |series| { series.clone() }); let pre_era_info = pre_snapshot.current_era_info; diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index ca00d988b8..ee12676228 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -311,6 +311,67 @@ fn account_ledger_staked_amount_works() { assert!(acc_ledger.staked_amount(period + 1).is_zero()); } +#[test] +fn account_ledger_staked_amount_for_type_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // 1st scenario - 'current' entry is set, 'future' is None + let (voting_1, build_and_earn_1, period) = (31, 43, 2); + acc_ledger.staked = StakeAmount { + voting: voting_1, + build_and_earn: build_and_earn_1, + era: 10, + period, + }; + acc_ledger.staked_future = None; + + // Correct period should return staked amounts + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, period), + voting_1 + ); + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::BuildAndEarn, period), + build_and_earn_1 + ); + + // Inocrrect period should simply return 0 + assert!(acc_ledger + .staked_amount_for_type(PeriodType::Voting, period - 1) + .is_zero()); + assert!(acc_ledger + .staked_amount_for_type(PeriodType::BuildAndEarn, period - 1) + .is_zero()); + + // 2nd scenario - both entries are set, but 'future' must be relevant one. + let (voting_2, build_and_earn_2, period) = (13, 19, 2); + acc_ledger.staked_future = Some(StakeAmount { + voting: voting_2, + build_and_earn: build_and_earn_2, + era: 20, + period, + }); + + // Correct period should return staked amounts + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, period), + voting_2 + ); + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::BuildAndEarn, period), + build_and_earn_2 + ); + + // Inocrrect period should simply return 0 + assert!(acc_ledger + .staked_amount_for_type(PeriodType::Voting, period - 1) + .is_zero()); + assert!(acc_ledger + .staked_amount_for_type(PeriodType::BuildAndEarn, period - 1) + .is_zero()); +} + #[test] fn account_ledger_stakeable_amount_works() { get_u32_type!(UnlockingDummy, 5); @@ -348,6 +409,52 @@ fn account_ledger_stakeable_amount_works() { ); } +#[test] +fn account_ledger_staked_era_period_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + let (era_1, period) = (10, 2); + let stake_amount_1 = StakeAmount { + voting: 13, + build_and_earn: 17, + era: era_1, + period, + }; + + // Sanity check, empty ledger + assert!(acc_ledger.staked_period().is_none()); + assert!(acc_ledger.earliest_staked_era().is_none()); + + // 1st scenario - only 'current' entry is set + acc_ledger.staked = stake_amount_1; + acc_ledger.staked_future = None; + + assert_eq!(acc_ledger.staked_period(), Some(period)); + assert_eq!(acc_ledger.earliest_staked_era(), Some(era_1)); + + // 2nd scenario - only 'future' is set + let era_2 = era_1 + 7; + let stake_amount_2 = StakeAmount { + voting: 13, + build_and_earn: 17, + era: era_2, + period, + }; + acc_ledger.staked = Default::default(); + acc_ledger.staked_future = Some(stake_amount_2); + + assert_eq!(acc_ledger.staked_period(), Some(period)); + assert_eq!(acc_ledger.earliest_staked_era(), Some(era_2)); + + // 3rd scenario - both entries are set + acc_ledger.staked = stake_amount_1; + acc_ledger.staked_future = Some(stake_amount_2); + + assert_eq!(acc_ledger.staked_period(), Some(period)); + assert_eq!(acc_ledger.earliest_staked_era(), Some(era_1)); +} + #[test] fn account_ledger_add_stake_amount_basic_example_works() { get_u32_type!(UnlockingDummy, 5); @@ -1172,47 +1279,47 @@ fn singular_staking_info_unstake_during_bep_is_ok() { } #[test] -fn contract_stake_amount_info_series_get_works() { +fn contract_stake_info_get_works() { let info_1 = StakeAmount::new(0, 0, 4, 2); let info_2 = StakeAmount::new(11, 0, 7, 3); - let series = ContractStakeAmountSeries { + let contract_stake = ContractStakeAmount { staked: info_1, staked_future: Some(info_2), }; // Sanity check - assert!(!series.is_empty()); + assert!(!contract_stake.is_empty()); // 1st scenario - get existing entries - assert_eq!(series.get(4, 2), Some(info_1)); - assert_eq!(series.get(7, 3), Some(info_2)); + assert_eq!(contract_stake.get(4, 2), Some(info_1)); + assert_eq!(contract_stake.get(7, 3), Some(info_2)); // 2nd scenario - get non-existing entries for covered eras { let era_1 = 6; - let entry_1 = series.get(era_1, 2).expect("Has to be Some"); + let entry_1 = contract_stake.get(era_1, 2).expect("Has to be Some"); assert!(entry_1.total().is_zero()); assert_eq!(entry_1.era, era_1); assert_eq!(entry_1.period, 2); let era_2 = 8; - let entry_1 = series.get(era_2, 3).expect("Has to be Some"); + let entry_1 = contract_stake.get(era_2, 3).expect("Has to be Some"); assert_eq!(entry_1.total(), 11); assert_eq!(entry_1.era, era_2); assert_eq!(entry_1.period, 3); } // 3rd scenario - get non-existing entries for covered eras but mismatching period - assert!(series.get(8, 2).is_none()); + assert!(contract_stake.get(8, 2).is_none()); // 4th scenario - get non-existing entries for non-covered eras - assert!(series.get(3, 2).is_none()); + assert!(contract_stake.get(3, 2).is_none()); } #[test] -fn contract_stake_amount_info_series_stake_is_ok() { - let mut series = ContractStakeAmountSeries::default(); +fn contract_stake_info_stake_is_ok() { + let mut contract_stake = ContractStakeAmount::default(); // 1st scenario - stake some amount and verify state change let era_1 = 3; @@ -1220,14 +1327,14 @@ fn contract_stake_amount_info_series_stake_is_ok() { let period_1 = 5; let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); let amount_1 = 31; - series.stake(amount_1, period_info_1, era_1); - assert!(!series.is_empty()); + contract_stake.stake(amount_1, period_info_1, era_1); + assert!(!contract_stake.is_empty()); assert!( - series.get(era_1, period_1).is_none(), + contract_stake.get(era_1, period_1).is_none(), "Entry for current era must not exist." ); - let entry_1_1 = series.get(stake_era_1, period_1).unwrap(); + let entry_1_1 = contract_stake.get(stake_era_1, period_1).unwrap(); assert_eq!( entry_1_1.era, stake_era_1, "Stake is only valid from next era." @@ -1236,8 +1343,8 @@ fn contract_stake_amount_info_series_stake_is_ok() { // 2nd scenario - stake some more to the same era but different period type, and verify state change. let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); - series.stake(amount_1, period_info_1, era_1); - let entry_1_2 = series.get(stake_era_1, period_1).unwrap(); + contract_stake.stake(amount_1, period_info_1, era_1); + let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); assert_eq!(entry_1_2.era, stake_era_1); assert_eq!(entry_1_2.total(), amount_1 * 2); @@ -1245,9 +1352,9 @@ fn contract_stake_amount_info_series_stake_is_ok() { let era_2 = era_1 + 2; let stake_era_2 = era_2 + 1; let amount_2 = 37; - series.stake(amount_2, period_info_1, era_2); - let entry_2_1 = series.get(stake_era_1, period_1).unwrap(); - let entry_2_2 = series.get(stake_era_2, period_1).unwrap(); + contract_stake.stake(amount_2, period_info_1, era_2); + let entry_2_1 = contract_stake.get(stake_era_1, period_1).unwrap(); + let entry_2_2 = contract_stake.get(stake_era_2, period_1).unwrap(); assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); assert_eq!(entry_2_2.era, stake_era_2); assert_eq!(entry_2_2.period, period_1); @@ -1264,16 +1371,16 @@ fn contract_stake_amount_info_series_stake_is_ok() { let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); let amount_3 = 41; - series.stake(amount_3, period_info_2, era_3); + contract_stake.stake(amount_3, period_info_2, era_3); assert!( - series.get(stake_era_1, period_1).is_none(), + contract_stake.get(stake_era_1, period_1).is_none(), "Old period must be removed." ); assert!( - series.get(stake_era_2, period_1).is_none(), + contract_stake.get(stake_era_2, period_1).is_none(), "Old period must be removed." ); - let entry_3_1 = series.get(stake_era_3, period_2).unwrap(); + let entry_3_1 = contract_stake.get(stake_era_3, period_2).unwrap(); assert_eq!(entry_3_1.era, stake_era_3); assert_eq!(entry_3_1.period, period_2); assert_eq!( @@ -1286,9 +1393,9 @@ fn contract_stake_amount_info_series_stake_is_ok() { let era_4 = era_3 + 1; let stake_era_4 = era_4 + 1; let amount_4 = 5; - series.stake(amount_4, period_info_2, era_4); - let entry_4_1 = series.get(stake_era_3, period_2).unwrap(); - let entry_4_2 = series.get(stake_era_4, period_2).unwrap(); + contract_stake.stake(amount_4, period_info_2, era_4); + let entry_4_1 = contract_stake.get(stake_era_3, period_2).unwrap(); + let entry_4_2 = contract_stake.get(stake_era_4, period_2).unwrap(); assert_eq!(entry_4_1, entry_3_1, "Old entry must remain unchanged."); assert_eq!(entry_4_2.era, stake_era_4); assert_eq!(entry_4_2.period, period_2); @@ -1296,22 +1403,25 @@ fn contract_stake_amount_info_series_stake_is_ok() { } #[test] -fn contract_stake_amount_info_series_unstake_is_ok() { - let mut series = ContractStakeAmountSeries::default(); +fn contract_stake_info_unstake_is_ok() { + let mut contract_stake = ContractStakeAmount::default(); // Prep action - create a stake entry let era_1 = 2; let period = 3; let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); let stake_amount = 100; - series.stake(stake_amount, period_info, era_1); + contract_stake.stake(stake_amount, period_info, era_1); // 1st scenario - unstake in the same era let amount_1 = 5; - series.unstake(amount_1, period_info, era_1); - assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); + contract_stake.unstake(amount_1, period_info, era_1); + assert_eq!( + contract_stake.total_staked_amount(period), + stake_amount - amount_1 + ); assert_eq!( - series.staked_amount(period, PeriodType::Voting), + contract_stake.staked_amount(period, PeriodType::Voting), stake_amount - amount_1 ); @@ -1319,13 +1429,13 @@ fn contract_stake_amount_info_series_unstake_is_ok() { let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); let era_2 = era_1 + 3; let amount_2 = 7; - series.unstake(amount_2, period_info, era_2); + contract_stake.unstake(amount_2, period_info, era_2); assert_eq!( - series.total_staked_amount(period), + contract_stake.total_staked_amount(period), stake_amount - amount_1 - amount_2 ); assert_eq!( - series.staked_amount(period, PeriodType::Voting), + contract_stake.staked_amount(period, PeriodType::Voting), stake_amount - amount_1 - amount_2 ); } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 9fda94b3a0..4c59a7d7a1 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -312,6 +312,10 @@ pub struct AccountLedger< /// Number of contract stake entries in storage. #[codec(compact)] pub contract_stake_count: u32, + + // TODO: introduce a variable which keeps track of the latest era for which the rewards have been calculated. + // This is needed since in case we break up reward calculation into multiple blocks, we should prohibit staking until + // reward calculation has finished. } impl Default for AccountLedger @@ -1005,13 +1009,22 @@ impl SingularStakingInfo { } } -/// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. +/// Composite type that holds information about how much was staked on a contract in up to two distinct eras. +/// +/// This is needed since 'stake' operation only makes the staked amount valid from the next era. +/// In a situation when `stake` is called in era `N`, the staked amount is valid from era `N+1`, hence the need for 'future' entry. +/// +/// **NOTE:** The 'future' entry term is only valid in the era when `stake` is called. It's possible contract stake isn't changed in consecutive eras, +/// so we might end up in a situation where era is `N + 10` but `staked` entry refers to era `N` and `staked_future` entry refers to era `N+1`. +/// This is still valid since these values are expected to be updated lazily. #[derive(Encode, Decode, MaxEncodedLen, RuntimeDebug, PartialEq, Eq, Clone, TypeInfo, Default)] -pub struct ContractStakeAmountSeries { +pub struct ContractStakeAmount { + /// Staked amount in the 'current' era. pub staked: StakeAmount, + /// Staked amount in the next or 'future' era. pub staked_future: Option, } -impl ContractStakeAmountSeries { +impl ContractStakeAmount { /// `true` if series is empty, `false` otherwise. pub fn is_empty(&self) -> bool { self.staked.is_empty() && self.staked_future.is_none() From c47d36e4fa1635d586f877ef2c9dc9b36d94a881 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 2 Nov 2023 15:42:02 +0100 Subject: [PATCH 43/86] Formatting --- pallets/dapp-staking-v3/src/lib.rs | 1 + pallets/dapp-staking-v3/src/test/testing_utils.rs | 4 +--- pallets/dapp-staking-v3/src/types.rs | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 060d691199..ae62162165 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1606,6 +1606,7 @@ 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. // Benchmarks will show this, but I don't believe it will be needed, especially with increased block capacity we'll get with async backing. // Even without async backing though, we should have enough capacity to handle this. + // UPDATE: might work with async backing, but right now we could handle up to 150 dApps before exceeding the PoV size. let tier_config = TierConfig::::get(); let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index fbd90823d2..499cef4d28 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -432,9 +432,7 @@ pub(crate) fn assert_stake( let pre_contract_stake = pre_snapshot .contract_stake .get(&smart_contract) - .map_or(ContractStakeAmount::default(), |series| { - series.clone() - }); + .map_or(ContractStakeAmount::default(), |series| series.clone()); let pre_era_info = pre_snapshot.current_era_info; let stake_era = pre_snapshot.active_protocol_state.era + 1; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 4c59a7d7a1..f8b0b264f8 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -312,7 +312,6 @@ pub struct AccountLedger< /// Number of contract stake entries in storage. #[codec(compact)] pub contract_stake_count: u32, - // TODO: introduce a variable which keeps track of the latest era for which the rewards have been calculated. // This is needed since in case we break up reward calculation into multiple blocks, we should prohibit staking until // reward calculation has finished. From d36839c20b93b93ed2a6c6fac38c5993c25ca065 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 2 Nov 2023 16:37:44 +0100 Subject: [PATCH 44/86] More benchmarks & experiments, refactoring --- pallets/dapp-staking-v3/src/benchmarking.rs | 19 +- pallets/dapp-staking-v3/src/dsv3_weight.rs | 55 ++- pallets/dapp-staking-v3/src/lib.rs | 56 +-- pallets/dapp-staking-v3/src/test/mock.rs | 8 +- .../dapp-staking-v3/src/test/testing_utils.rs | 52 +-- pallets/dapp-staking-v3/src/test/tests.rs | 33 +- .../dapp-staking-v3/src/test/tests_types.rs | 318 ++++++++++++------ pallets/dapp-staking-v3/src/types.rs | 256 +++++++------- 8 files changed, 490 insertions(+), 307 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 5f71f00ee3..ac0ac838c7 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -74,9 +74,9 @@ pub(crate) fn advance_to_next_era() { // } // /// Advance blocks until next period type has been reached. -// pub(crate) fn advance_to_next_period_type() { -// let period_type = ActiveProtocolState::::get().period_type(); -// while ActiveProtocolState::::get().period_type() == period_type { +// pub(crate) fn advance_to_next_subperiod() { +// let subperiod = ActiveProtocolState::::get().subperiod(); +// while ActiveProtocolState::::get().subperiod() == subperiod { // run_for_blocks::(One::one()); // } // } @@ -99,7 +99,7 @@ pub fn initial_config() { next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + One::one(), period_info: PeriodInfo { number: 1, - period_type: PeriodType::Voting, + subperiod: Subperiod::Voting, ending_era: 2, }, maintenance: false, @@ -217,6 +217,17 @@ mod benchmarks { } } + #[benchmark] + fn experimental_read() { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + #[block] + { + let _ = ExperimentalContractEntries::::get(10); + } + } + impl_benchmark_test_suite!( Pallet, crate::benchmarking::tests::new_test_ext(), diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs index 64948127b5..1dca8a56ce 100644 --- a/pallets/dapp-staking-v3/src/dsv3_weight.rs +++ b/pallets/dapp-staking-v3/src/dsv3_weight.rs @@ -20,7 +20,7 @@ //! Autogenerated weights for pallet_dapp_staking_v3 //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-30, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-11-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `Dinos-MBP`, CPU: `` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -50,6 +50,7 @@ use core::marker::PhantomData; /// Weight functions needed for pallet_dapp_staking_v3. pub trait WeightInfo { fn dapp_tier_assignment(x: u32, ) -> Weight; + fn experimental_read() -> Weight; } /// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. @@ -62,19 +63,29 @@ impl WeightInfo for SubstrateWeight { /// Storage: DappStaking IntegratedDApps (r:101 w:0) /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:100 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(170), added: 2645, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `823 + x * (164 ±0)` - // Estimated: `3586 + x * (2645 ±0)` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(12_342_575, 3586) - // Standard Error: 16_840 - .saturating_add(Weight::from_parts(7_051_078, 0).saturating_mul(x.into())) + // Measured: `836 + x * (169 ±0)` + // Estimated: `3586 + x * (2605 ±0)` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(12_879_631, 3586) + // Standard Error: 18_480 + .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2645).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) + } + /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) + /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) + fn experimental_read() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `6948` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(5_000_000, 6948) + .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -87,18 +98,28 @@ impl WeightInfo for () { /// Storage: DappStaking IntegratedDApps (r:101 w:0) /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:100 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(170), added: 2645, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `823 + x * (164 ±0)` - // Estimated: `3586 + x * (2645 ±0)` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(12_342_575, 3586) - // Standard Error: 16_840 - .saturating_add(Weight::from_parts(7_051_078, 0).saturating_mul(x.into())) + // Measured: `836 + x * (169 ±0)` + // Estimated: `3586 + x * (2605 ±0)` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(12_879_631, 3586) + // Standard Error: 18_480 + .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2645).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) + } + /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) + /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) + fn experimental_read() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `6948` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(5_000_000, 6948) + .saturating_add(RocksDbWeight::get().reads(1_u64)) } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index ae62162165..8f9797bb9f 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -178,7 +178,7 @@ pub mod pallet { NewEra { era: EraNumber }, /// New period has started. NewPeriod { - period_type: PeriodType, + subperiod: Subperiod, number: PeriodNumber, }, /// A smart contract has been registered for dApp staking @@ -416,6 +416,11 @@ pub mod pallet { pub type DAppTiers = StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; + // TODO: this is experimental, please don't review + #[pallet::storage] + pub type ExperimentalContractEntries = + StorageMap<_, Twox64Concat, EraNumber, ContractEntriesFor, OptionQuery>; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -473,7 +478,7 @@ pub mod pallet { next_era_start: Pallet::::blocks_per_voting_period() + 1_u32.into(), period_info: PeriodInfo { number: 1, - period_type: PeriodType::Voting, + subperiod: Subperiod::Voting, ending_era: 2, }, maintenance: false, @@ -510,8 +515,8 @@ pub mod pallet { let current_era = protocol_state.era; let next_era = current_era.saturating_add(1); - let (maybe_period_event, era_reward) = match protocol_state.period_type() { - PeriodType::Voting => { + let (maybe_period_event, era_reward) = match protocol_state.subperiod() { + Subperiod::Voting => { // For the sake of consistency, we put zero reward into storage let era_reward = EraReward { staker_reward_pool: Balance::zero(), @@ -523,9 +528,9 @@ pub mod pallet { next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.into_next_period_type(ending_era, build_and_earn_start_block); + protocol_state.into_next_subperiod(ending_era, build_and_earn_start_block); - era_info.migrate_to_next_era(Some(protocol_state.period_type())); + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); // Update tier configuration to be used when calculating rewards for the upcoming eras let next_tier_config = NextTierConfig::::take(); @@ -533,13 +538,13 @@ pub mod pallet { ( Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), + subperiod: protocol_state.subperiod(), number: protocol_state.period_number(), }), era_reward, ) } - PeriodType::BuildAndEarn => { + Subperiod::BuildAndEarn => { let (staker_reward_pool, dapp_reward_pool) = T::RewardPoolProvider::normal_reward_pools(); let era_reward = EraReward { @@ -564,7 +569,7 @@ pub mod pallet { &protocol_state.period_number(), PeriodEndInfo { bonus_reward_pool, - total_vp_stake: era_info.staked_amount(PeriodType::Voting), + total_vp_stake: era_info.staked_amount(Subperiod::Voting), final_era: current_era, }, ); @@ -575,9 +580,9 @@ pub mod pallet { let voting_period_length = Self::blocks_per_voting_period(); let next_era_start_block = now.saturating_add(voting_period_length); - protocol_state.into_next_period_type(ending_era, next_era_start_block); + protocol_state.into_next_subperiod(ending_era, next_era_start_block); - era_info.migrate_to_next_era(Some(protocol_state.period_type())); + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); // Re-calculate tier configuration for the upcoming new period let tier_params = StaticTierParams::::get(); @@ -588,7 +593,7 @@ pub mod pallet { ( Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), + subperiod: protocol_state.subperiod(), number: protocol_state.period_number(), }), era_reward, @@ -1045,12 +1050,12 @@ pub mod pallet { None => ( SingularStakingInfo::new( protocol_state.period_number(), - protocol_state.period_type(), + protocol_state.subperiod(), ), true, ), }; - new_staking_info.stake(amount, protocol_state.period_type()); + new_staking_info.stake(amount, protocol_state.subperiod()); ensure!( new_staking_info.total_staked_amount() >= T::MinimumStakeAmount::get(), Error::::InsufficientStakeAmount @@ -1072,7 +1077,7 @@ pub mod pallet { // 4. // Update total staked amount for the next era. CurrentEraInfo::::mutate(|era_info| { - era_info.add_stake_amount(amount, protocol_state.period_type()); + era_info.add_stake_amount(amount, protocol_state.subperiod()); }); // 5. @@ -1139,7 +1144,7 @@ pub mod pallet { amount }; - staking_info.unstake(amount, protocol_state.period_type()); + staking_info.unstake(amount, protocol_state.subperiod()); (staking_info, amount) } None => { @@ -1170,7 +1175,7 @@ pub mod pallet { // 4. // Update total staked amount for the next era. CurrentEraInfo::::mutate(|era_info| { - era_info.unstake_amount(amount, protocol_state.period_type()); + era_info.unstake_amount(amount, protocol_state.subperiod()); }); // 5. @@ -1323,7 +1328,7 @@ pub mod pallet { Error::::InternalClaimBonusError ); - let eligible_amount = staker_info.staked_amount(PeriodType::Voting); + let eligible_amount = staker_info.staked_amount(Subperiod::Voting); let bonus_reward = Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; @@ -1380,7 +1385,6 @@ pub mod pallet { })?; // Get reward destination, and deposit the reward. - // TODO: should we check reward is greater than zero, or even more precise, it's greater than the existential deposit? Seems redundant but still... let beneficiary = dapp_info.reward_beneficiary(); T::Currency::deposit_creating(beneficiary, amount); @@ -1447,7 +1451,7 @@ pub mod pallet { // Update total staked amount for the next era. // This means 'fake' stake total amount has been kept until now, even though contract was unregistered. CurrentEraInfo::::mutate(|era_info| { - era_info.unstake_amount(amount, protocol_state.period_type()); + era_info.unstake_amount(amount, protocol_state.subperiod()); }); // Update remaining storage entries @@ -1517,7 +1521,7 @@ pub mod pallet { match force_type { ForcingType::Era => (), - ForcingType::PeriodType => { + ForcingType::Subperiod => { state.period_info.ending_era = state.era.saturating_add(1); } } @@ -1608,6 +1612,13 @@ pub mod pallet { // Even without async backing though, we should have enough capacity to handle this. // UPDATE: might work with async backing, but right now we could handle up to 150 dApps before exceeding the PoV size. + // UPDATE2: instead of taking the approach of reading an ever increasing amount of entries from storage, we can instead adopt an approach + // of eficiently storing composite information into `BTreeMap`. The approach is essentially the same as the one used below to store rewards. + // Each time `stake` or `unstake` are called, corresponding entries are updated. This way we can keep track of all the contract stake in a single DB entry. + // To make the solution more scalable, we could 'split' stake entries into spans, similar as rewards are handled now. + // + // Experiment with an 'experimental' entry shows PoV size of ~7kB induced for entry that can hold up to 100 entries. + let tier_config = TierConfig::::get(); let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); @@ -1676,6 +1687,9 @@ pub mod pallet { // 4. // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids). + // TODO & Idea: perhaps use BTreeMap instead? It will "sort" automatically based on dApp Id, and we can efficiently remove entries, + // reducing PoV size step by step. + // It's a trade-off between speed and PoV size. Although both are quite minor, so maybe it doesn't matter that much. dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); // 5. Calculate rewards. diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 8dc7be2a08..a2fdd20a26 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -204,7 +204,7 @@ impl ExtBuilder { next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, period_info: PeriodInfo { number: 1, - period_type: PeriodType::Voting, + subperiod: Subperiod::Voting, ending_era: 2, }, maintenance: false, @@ -304,9 +304,9 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_into_next_period_type() { - let period_type = ActiveProtocolState::::get().period_type(); - while ActiveProtocolState::::get().period_type() == period_type { +pub(crate) fn advance_to_into_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { run_for_blocks(1); } } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 499cef4d28..3ac63253ab 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -437,7 +437,7 @@ pub(crate) fn assert_stake( let stake_era = pre_snapshot.active_protocol_state.era + 1; let stake_period = pre_snapshot.active_protocol_state.period_number(); - let stake_period_type = pre_snapshot.active_protocol_state.period_type(); + let stake_subperiod = pre_snapshot.active_protocol_state.subperiod(); // Stake on smart contract & verify event assert_ok!(DappStaking::stake( @@ -496,8 +496,8 @@ pub(crate) fn assert_stake( "Total staked amount must increase by the 'amount'" ); assert_eq!( - post_staker_info.staked_amount(stake_period_type), - pre_staker_info.staked_amount(stake_period_type) + amount, + post_staker_info.staked_amount(stake_subperiod), + pre_staker_info.staked_amount(stake_subperiod) + amount, "Staked amount must increase by the 'amount'" ); assert_eq!(post_staker_info.period_number(), stake_period); @@ -516,14 +516,14 @@ pub(crate) fn assert_stake( ); assert!(amount >= ::MinimumStakeAmount::get()); assert_eq!( - post_staker_info.staked_amount(stake_period_type), + post_staker_info.staked_amount(stake_subperiod), amount, "Staked amount must be equal to exactly the 'amount'" ); assert_eq!(post_staker_info.period_number(), stake_period); assert_eq!( post_staker_info.is_loyal(), - stake_period_type == PeriodType::Voting + stake_subperiod == Subperiod::Voting ); } } @@ -537,8 +537,8 @@ pub(crate) fn assert_stake( "Staked amount must increase by the 'amount'" ); assert_eq!( - post_contract_stake.staked_amount(stake_period, stake_period_type), - pre_contract_stake.staked_amount(stake_period, stake_period_type) + amount, + post_contract_stake.staked_amount(stake_period, stake_subperiod), + pre_contract_stake.staked_amount(stake_period, stake_subperiod) + amount, "Staked amount must increase by the 'amount'" ); @@ -561,8 +561,8 @@ pub(crate) fn assert_stake( pre_era_info.total_staked_amount_next_era() + amount ); assert_eq!( - post_era_info.staked_amount_next_era(stake_period_type), - pre_era_info.staked_amount_next_era(stake_period_type) + amount + post_era_info.staked_amount_next_era(stake_subperiod), + pre_era_info.staked_amount_next_era(stake_subperiod) + amount ); } @@ -586,7 +586,7 @@ pub(crate) fn assert_unstake( let _unstake_era = pre_snapshot.active_protocol_state.era; let unstake_period = pre_snapshot.active_protocol_state.period_number(); - let unstake_period_type = pre_snapshot.active_protocol_state.period_type(); + let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod(); let minimum_stake_amount: Balance = ::MinimumStakeAmount::get(); let is_full_unstake = @@ -655,17 +655,17 @@ pub(crate) fn assert_unstake( "Total staked amount must decrease by the 'amount'" ); assert_eq!( - post_staker_info.staked_amount(unstake_period_type), + post_staker_info.staked_amount(unstake_subperiod), pre_staker_info - .staked_amount(unstake_period_type) + .staked_amount(unstake_subperiod) .saturating_sub(amount), "Staked amount must decrease by the 'amount'" ); let is_loyal = pre_staker_info.is_loyal() - && !(unstake_period_type == PeriodType::BuildAndEarn - && post_staker_info.staked_amount(PeriodType::Voting) - < pre_staker_info.staked_amount(PeriodType::Voting)); + && !(unstake_subperiod == Subperiod::BuildAndEarn + && post_staker_info.staked_amount(Subperiod::Voting) + < pre_staker_info.staked_amount(Subperiod::Voting)); assert_eq!( post_staker_info.is_loyal(), is_loyal, @@ -682,9 +682,9 @@ pub(crate) fn assert_unstake( "Staked amount must decreased by the 'amount'" ); assert_eq!( - post_contract_stake.staked_amount(unstake_period, unstake_period_type), + post_contract_stake.staked_amount(unstake_period, unstake_subperiod), pre_contract_stake - .staked_amount(unstake_period, unstake_period_type) + .staked_amount(unstake_period, unstake_subperiod) .saturating_sub(amount), "Staked amount must decreased by the 'amount'" ); @@ -709,22 +709,22 @@ pub(crate) fn assert_unstake( "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." ); - if unstake_period_type == PeriodType::BuildAndEarn - && pre_era_info.staked_amount_next_era(PeriodType::BuildAndEarn) < amount + if unstake_subperiod == Subperiod::BuildAndEarn + && pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn) < amount { - let overflow = amount - pre_era_info.staked_amount_next_era(PeriodType::BuildAndEarn); + let overflow = amount - pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn); assert!(post_era_info - .staked_amount_next_era(PeriodType::BuildAndEarn) + .staked_amount_next_era(Subperiod::BuildAndEarn) .is_zero()); assert_eq!( - post_era_info.staked_amount_next_era(PeriodType::Voting), - pre_era_info.staked_amount_next_era(PeriodType::Voting) - overflow + post_era_info.staked_amount_next_era(Subperiod::Voting), + pre_era_info.staked_amount_next_era(Subperiod::Voting) - overflow ); } else { assert_eq!( - post_era_info.staked_amount_next_era(unstake_period_type), - pre_era_info.staked_amount_next_era(unstake_period_type) - amount + post_era_info.staked_amount_next_era(unstake_subperiod), + pre_era_info.staked_amount_next_era(unstake_subperiod) - amount ); } } @@ -866,7 +866,7 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId, smart_contract: &Moc let pre_free_balance = ::Currency::free_balance(&account); let staked_period = pre_staker_info.period_number(); - let stake_amount = pre_staker_info.staked_amount(PeriodType::Voting); + let stake_amount = pre_staker_info.staked_amount(Subperiod::Voting); let period_end_info = pre_snapshot .period_end diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 4b23f8c784..acc842314e 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -20,7 +20,7 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, - IntegratedDApps, Ledger, NextDAppId, PeriodNumber, PeriodType, + IntegratedDApps, Ledger, NextDAppId, PeriodNumber, Subperiod, }; use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; @@ -34,8 +34,13 @@ fn print_test() { ExtBuilder::build().execute_with(|| { use crate::dsv3_weight::WeightInfo; println!( - ">>> {:?}", - crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(200) + ">>> dApp tier assignment reading & calculation {:?}", + crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(100) + ); + + println!( + ">>> Experimental storage entry read {:?}", + crate::dsv3_weight::SubstrateWeight::::experimental_read() ); }) } @@ -156,7 +161,7 @@ fn on_initialize_state_change_works() { let protocol_state = ActiveProtocolState::::get(); assert_eq!(protocol_state.era, 1); assert_eq!(protocol_state.period_number(), 1); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!(System::block_number(), 1); let blocks_per_voting_period = DappStaking::blocks_per_voting_period(); @@ -170,15 +175,15 @@ fn on_initialize_state_change_works() { run_to_block(protocol_state.next_era_start - 1); let protocol_state = ActiveProtocolState::::get(); assert_eq!( - protocol_state.period_type(), - PeriodType::Voting, + protocol_state.subperiod(), + Subperiod::Voting, "Period type should still be the same." ); assert_eq!(protocol_state.era, 1); run_for_blocks(1); let protocol_state = ActiveProtocolState::::get(); - assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!(protocol_state.era, 2); assert_eq!(protocol_state.period_number(), 1); @@ -191,7 +196,7 @@ fn on_initialize_state_change_works() { advance_to_next_era(); assert_eq!(System::block_number(), pre_block + blocks_per_era); let protocol_state = ActiveProtocolState::::get(); - assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!(protocol_state.period_number(), 1); assert_eq!(protocol_state.era, era + 1); } @@ -199,7 +204,7 @@ fn on_initialize_state_change_works() { // Finaly advance over to the next era and ensure we're back to voting period advance_to_next_era(); let protocol_state = ActiveProtocolState::::get(); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!(protocol_state.era, 2 + eras_per_bep_period); assert_eq!( protocol_state.next_era_start, @@ -864,7 +869,7 @@ fn stake_in_final_era_fails() { // Force Build&Earn period ActiveProtocolState::::mutate(|state| { - state.period_info.period_type = PeriodType::BuildAndEarn; + state.period_info.subperiod = Subperiod::BuildAndEarn; state.period_info.ending_era = state.era + 1; }); @@ -1287,7 +1292,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_period( ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); - advance_to_into_next_period_type(); + advance_to_into_next_subperiod(); advance_to_era(ActiveProtocolState::::get().period_info.ending_era - 1); assert_claim_staker_rewards(account); @@ -1412,10 +1417,10 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { assert_lock(account, lock_amount); // Stake in Build&Earn period type, advance to next era and try to claim bonus reward - advance_to_into_next_period_type(); + advance_to_into_next_subperiod(); assert_eq!( - ActiveProtocolState::::get().period_type(), - PeriodType::BuildAndEarn, + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, "Sanity check." ); let stake_amount = 93; diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index ee12676228..a3d3e7d5b9 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -37,20 +37,24 @@ macro_rules! get_u32_type { } #[test] -fn period_type_sanity_check() { - assert_eq!(PeriodType::Voting.next(), PeriodType::BuildAndEarn); - assert_eq!(PeriodType::BuildAndEarn.next(), PeriodType::Voting); +fn subperiod_sanity_check() { + assert_eq!(Subperiod::Voting.next(), Subperiod::BuildAndEarn); + assert_eq!(Subperiod::BuildAndEarn.next(), Subperiod::Voting); } #[test] fn period_info_basic_checks() { let period_number = 2; let ending_era = 5; - let info = PeriodInfo::new(period_number, PeriodType::Voting, ending_era); + let info = PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + ending_era: ending_era, + }; // Sanity checks assert_eq!(info.number, period_number); - assert_eq!(info.period_type, PeriodType::Voting); + assert_eq!(info.subperiod, Subperiod::Voting); assert_eq!(info.ending_era, ending_era); // Voting period checks @@ -65,7 +69,11 @@ fn period_info_basic_checks() { } // Build&Earn period checks - let info = PeriodInfo::new(period_number, PeriodType::BuildAndEarn, ending_era); + let info = PeriodInfo { + number: period_number, + subperiod: Subperiod::BuildAndEarn, + ending_era: ending_era, + }; assert!(!info.is_next_period(ending_era - 1)); assert!(info.is_next_period(ending_era)); assert!(info.is_next_period(ending_era + 1)); @@ -88,11 +96,15 @@ fn protocol_state_basic_checks() { let period_number = 5; let ending_era = 11; let next_era_start = 31; - protocol_state.period_info = PeriodInfo::new(period_number, PeriodType::Voting, ending_era); + protocol_state.period_info = PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + ending_era: ending_era, + }; protocol_state.next_era_start = next_era_start; assert_eq!(protocol_state.period_number(), period_number); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); // New era check assert!(!protocol_state.is_new_era(next_era_start - 1)); @@ -102,8 +114,8 @@ fn protocol_state_basic_checks() { // Toggle new period type check - 'Voting' to 'BuildAndEarn' let ending_era_1 = 23; let next_era_start_1 = 41; - protocol_state.into_next_period_type(ending_era_1, next_era_start_1); - assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + protocol_state.into_next_subperiod(ending_era_1, next_era_start_1); + assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!( protocol_state.period_number(), period_number, @@ -116,8 +128,8 @@ fn protocol_state_basic_checks() { // Toggle from 'BuildAndEarn' over to 'Voting' let ending_era_2 = 24; let next_era_start_2 = 91; - protocol_state.into_next_period_type(ending_era_2, next_era_start_2); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + protocol_state.into_next_subperiod(ending_era_2, next_era_start_2); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!( protocol_state.period_number(), period_number + 1, @@ -328,20 +340,20 @@ fn account_ledger_staked_amount_for_type_works() { // Correct period should return staked amounts assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, period), + acc_ledger.staked_amount_for_type(Subperiod::Voting, period), voting_1 ); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::BuildAndEarn, period), + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period), build_and_earn_1 ); // Inocrrect period should simply return 0 assert!(acc_ledger - .staked_amount_for_type(PeriodType::Voting, period - 1) + .staked_amount_for_type(Subperiod::Voting, period - 1) .is_zero()); assert!(acc_ledger - .staked_amount_for_type(PeriodType::BuildAndEarn, period - 1) + .staked_amount_for_type(Subperiod::BuildAndEarn, period - 1) .is_zero()); // 2nd scenario - both entries are set, but 'future' must be relevant one. @@ -355,20 +367,20 @@ fn account_ledger_staked_amount_for_type_works() { // Correct period should return staked amounts assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, period), + acc_ledger.staked_amount_for_type(Subperiod::Voting, period), voting_2 ); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::BuildAndEarn, period), + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period), build_and_earn_2 ); // Inocrrect period should simply return 0 assert!(acc_ledger - .staked_amount_for_type(PeriodType::Voting, period - 1) + .staked_amount_for_type(Subperiod::Voting, period - 1) .is_zero()); assert!(acc_ledger - .staked_amount_for_type(PeriodType::BuildAndEarn, period - 1) + .staked_amount_for_type(Subperiod::BuildAndEarn, period - 1) .is_zero()); } @@ -463,7 +475,15 @@ fn account_ledger_add_stake_amount_basic_example_works() { // Sanity check let period_number = 2; assert!(acc_ledger - .add_stake_amount(0, 0, PeriodInfo::new(period_number, PeriodType::Voting, 0)) + .add_stake_amount( + 0, + 0, + PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + ending_era: 0 + } + ) .is_ok()); assert!(acc_ledger.staked.is_empty()); assert!(acc_ledger.staked_future.is_none()); @@ -471,7 +491,11 @@ fn account_ledger_add_stake_amount_basic_example_works() { // 1st scenario - stake some amount in Voting period, and ensure values are as expected. let first_era = 1; let period_1 = 1; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + ending_era: 100, + }; let lock_amount = 17; let stake_amount = 11; acc_ledger.add_lock_amount(lock_amount); @@ -495,26 +519,30 @@ fn account_ledger_add_stake_amount_basic_example_works() { assert!(acc_ledger.staked_future.unwrap().build_and_earn.is_zero()); assert_eq!(acc_ledger.staked_amount(period_1), stake_amount); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), + acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), stake_amount ); assert!(acc_ledger - .staked_amount_for_type(PeriodType::BuildAndEarn, period_1) + .staked_amount_for_type(Subperiod::BuildAndEarn, period_1) .is_zero()); // Second scenario - stake some more, but to the next period type let snapshot = acc_ledger.staked; - let period_info_2 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + let period_info_2 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + ending_era: 100, + }; assert!(acc_ledger .add_stake_amount(1, first_era, period_info_2) .is_ok()); assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), + acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), stake_amount ); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::BuildAndEarn, period_1), + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period_1), 1 ); assert_eq!(acc_ledger.staked, snapshot); @@ -528,7 +556,11 @@ fn account_ledger_add_stake_amount_advanced_example_works() { // 1st scenario - stake some amount, and ensure values are as expected. let first_era = 1; let period_1 = 1; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + ending_era: 100, + }; let lock_amount = 17; let stake_amount_1 = 11; acc_ledger.add_lock_amount(lock_amount); @@ -550,14 +582,14 @@ fn account_ledger_add_stake_amount_advanced_example_works() { "This entry must remain unchanged." ); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), + acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), stake_amount_1 + stake_amount_2 ); assert_eq!( acc_ledger .staked_future .unwrap() - .for_type(PeriodType::Voting), + .for_type(Subperiod::Voting), stake_amount_1 + stake_amount_2 ); assert_eq!(acc_ledger.staked_future.unwrap().era, first_era + 1); @@ -571,7 +603,11 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { // Prep actions let first_era = 5; let period_1 = 2; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + ending_era: 100, + }; let lock_amount = 13; let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); @@ -590,7 +626,11 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { acc_ledger.add_stake_amount( 1, first_era, - PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + ending_era: 100 + } ), Err(AccountLedgerError::InvalidPeriod) ); @@ -607,7 +647,11 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { acc_ledger.add_stake_amount( 1, first_era, - PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + ending_era: 100 + } ), Err(AccountLedgerError::InvalidPeriod) ); @@ -620,14 +664,26 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { // Sanity check assert_eq!( - acc_ledger.add_stake_amount(10, 1, PeriodInfo::new(1, PeriodType::Voting, 100)), + acc_ledger.add_stake_amount( + 10, + 1, + PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + ending_era: 100 + } + ), Err(AccountLedgerError::UnavailableStakeFunds) ); // Lock some amount, and try to stake more than that let first_era = 5; let period_1 = 2; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + ending_era: 100, + }; let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); assert_eq!( @@ -654,7 +710,11 @@ fn account_ledger_unstake_amount_basic_scenario_works() { let amount_1 = 19; let era_1 = 2; let period_1 = 1; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + ending_era: 100, + }; acc_ledger.add_lock_amount(amount_1); let mut acc_ledger_2 = acc_ledger.clone(); @@ -701,7 +761,11 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { let amount_1 = 19; let era_1 = 2; let period_1 = 1; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + ending_era: 100, + }; acc_ledger.add_lock_amount(amount_1); // We have two entries at once @@ -719,20 +783,20 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { ); assert_eq!( - acc_ledger.staked.for_type(PeriodType::Voting), + acc_ledger.staked.for_type(Subperiod::Voting), amount_1 - 1 - 3 ); assert_eq!( acc_ledger .staked_future .unwrap() - .for_type(PeriodType::Voting), + .for_type(Subperiod::Voting), amount_1 - 3 ); assert!(acc_ledger .staked_future .unwrap() - .for_type(PeriodType::BuildAndEarn) + .for_type(Subperiod::BuildAndEarn) .is_zero()); // 2nd scenario - perform full unstake @@ -755,7 +819,7 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { acc_ledger .staked_future .unwrap() - .for_type(PeriodType::BuildAndEarn), + .for_type(Subperiod::BuildAndEarn), amount_2 ); } @@ -769,7 +833,11 @@ fn account_ledger_unstake_from_invalid_era_fails() { let amount_1 = 13; let era_1 = 2; let period_1 = 1; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + ending_era: 100, + }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger .add_stake_amount(amount_1, era_1, period_info_1) @@ -786,7 +854,11 @@ fn account_ledger_unstake_from_invalid_era_fails() { acc_ledger.unstake_amount( 1, era_1, - PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + ending_era: 100 + } ), Err(AccountLedgerError::InvalidPeriod) ); @@ -803,7 +875,11 @@ fn account_ledger_unstake_from_invalid_era_fails() { acc_ledger.unstake_amount( 1, era_1, - PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + ending_era: 100 + } ), Err(AccountLedgerError::InvalidPeriod) ); @@ -818,7 +894,11 @@ fn account_ledger_unstake_too_much_fails() { let amount_1 = 23; let era_1 = 2; let period_1 = 1; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + ending_era: 100, + }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger .add_stake_amount(amount_1, era_1, period_info_1) @@ -847,7 +927,11 @@ fn account_ledger_unlockable_amount_works() { // Some amount is staked, period matches let stake_period = 5; let stake_amount = 17; - let period_info = PeriodInfo::new(stake_period, PeriodType::Voting, 100); + let period_info = PeriodInfo { + number: stake_period, + subperiod: Subperiod::Voting, + ending_era: 100, + }; assert!(acc_ledger .add_stake_amount(stake_amount, lock_era, period_info) .is_ok()); @@ -985,10 +1069,10 @@ fn era_info_stake_works() { // Add some voting period stake let vp_stake_amount = 7; - era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); + era_info.add_stake_amount(vp_stake_amount, Subperiod::Voting); assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), + era_info.staked_amount_next_era(Subperiod::Voting), vp_stake_amount ); assert!( @@ -998,13 +1082,13 @@ fn era_info_stake_works() { // Add some build&earn period stake let bep_stake_amount = 13; - era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); + era_info.add_stake_amount(bep_stake_amount, Subperiod::BuildAndEarn); assert_eq!( era_info.total_staked_amount_next_era(), vp_stake_amount + bep_stake_amount ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + era_info.staked_amount_next_era(Subperiod::BuildAndEarn), bep_stake_amount ); assert!( @@ -1032,15 +1116,15 @@ fn era_info_unstake_works() { // 1st scenario - unstake some amount, no overflow let unstake_amount_1 = bep_stake_amount_1; - era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); + era_info.unstake_amount(unstake_amount_1, Subperiod::BuildAndEarn); // Current era assert_eq!( era_info.total_staked_amount(), total_staked - unstake_amount_1 ); - assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); - assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); + assert_eq!(era_info.staked_amount(Subperiod::Voting), vp_stake_amount); + assert!(era_info.staked_amount(Subperiod::BuildAndEarn).is_zero()); // Next era assert_eq!( @@ -1048,18 +1132,18 @@ fn era_info_unstake_works() { total_staked_next_era - unstake_amount_1 ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), + era_info.staked_amount_next_era(Subperiod::Voting), vp_stake_amount ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + era_info.staked_amount_next_era(Subperiod::BuildAndEarn), bep_stake_amount_2 - unstake_amount_1 ); // 2nd scenario - unstake some more, but with overflow let overflow = 2; let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; - era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); + era_info.unstake_amount(unstake_amount_2, Subperiod::BuildAndEarn); // Current era assert_eq!( @@ -1073,11 +1157,11 @@ fn era_info_unstake_works() { vp_stake_amount - overflow ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), + era_info.staked_amount_next_era(Subperiod::Voting), vp_stake_amount - overflow ); assert!(era_info - .staked_amount_next_era(PeriodType::BuildAndEarn) + .staked_amount_next_era(Subperiod::BuildAndEarn) .is_zero()); } @@ -1087,101 +1171,101 @@ fn stake_amount_works() { // Sanity check assert!(stake_amount.total().is_zero()); - assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + assert!(stake_amount.for_type(Subperiod::Voting).is_zero()); + assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); // Stake some amount in voting period let vp_stake_1 = 11; - stake_amount.add(vp_stake_1, PeriodType::Voting); + stake_amount.add(vp_stake_1, Subperiod::Voting); assert_eq!(stake_amount.total(), vp_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + assert_eq!(stake_amount.for_type(Subperiod::Voting), vp_stake_1); + assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); // Stake some amount in build&earn period let bep_stake_1 = 13; - stake_amount.add(bep_stake_1, PeriodType::BuildAndEarn); + stake_amount.add(bep_stake_1, Subperiod::BuildAndEarn); assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + assert_eq!(stake_amount.for_type(Subperiod::Voting), vp_stake_1); + assert_eq!(stake_amount.for_type(Subperiod::BuildAndEarn), bep_stake_1); // Unstake some amount from voting period let vp_unstake_1 = 5; - stake_amount.subtract(5, PeriodType::Voting); + stake_amount.subtract(5, Subperiod::Voting); assert_eq!( stake_amount.total(), vp_stake_1 + bep_stake_1 - vp_unstake_1 ); assert_eq!( - stake_amount.for_type(PeriodType::Voting), + stake_amount.for_type(Subperiod::Voting), vp_stake_1 - vp_unstake_1 ); - assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + assert_eq!(stake_amount.for_type(Subperiod::BuildAndEarn), bep_stake_1); // Unstake some amount from build&earn period let bep_unstake_1 = 2; - stake_amount.subtract(bep_unstake_1, PeriodType::BuildAndEarn); + stake_amount.subtract(bep_unstake_1, Subperiod::BuildAndEarn); assert_eq!( stake_amount.total(), vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 ); assert_eq!( - stake_amount.for_type(PeriodType::Voting), + stake_amount.for_type(Subperiod::Voting), vp_stake_1 - vp_unstake_1 ); assert_eq!( - stake_amount.for_type(PeriodType::BuildAndEarn), + stake_amount.for_type(Subperiod::BuildAndEarn), bep_stake_1 - bep_unstake_1 ); // Unstake some more from build&earn period, and chip away from the voting period let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; - stake_amount.subtract(bep_unstake_2, PeriodType::BuildAndEarn); + stake_amount.subtract(bep_unstake_2, Subperiod::BuildAndEarn); assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); assert_eq!( - stake_amount.for_type(PeriodType::Voting), + stake_amount.for_type(Subperiod::Voting), vp_stake_1 - vp_unstake_1 - 1 ); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); } #[test] fn singular_staking_info_basics_are_ok() { let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); + let subperiod = Subperiod::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Sanity checks assert_eq!(staking_info.period_number(), period_number); assert!(staking_info.is_loyal()); assert!(staking_info.total_staked_amount().is_zero()); - assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); + assert!(!SingularStakingInfo::new(period_number, Subperiod::BuildAndEarn).is_loyal()); // Add some staked amount during `Voting` period let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + staking_info.stake(vote_stake_amount_1, Subperiod::Voting); assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 ); assert!(staking_info - .staked_amount(PeriodType::BuildAndEarn) + .staked_amount(Subperiod::BuildAndEarn) .is_zero()); // Add some staked amount during `BuildAndEarn` period let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); assert_eq!( staking_info.total_staked_amount(), vote_stake_amount_1 + bep_stake_amount_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::BuildAndEarn), + staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 ); } @@ -1189,17 +1273,17 @@ fn singular_staking_info_basics_are_ok() { #[test] fn singular_staking_info_unstake_during_voting_is_ok() { let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); + let subperiod = Subperiod::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + staking_info.stake(vote_stake_amount_1, Subperiod::Voting); // Unstake some amount during `Voting` period, loyalty should remain as expected. let unstake_amount_1 = 5; assert_eq!( - staking_info.unstake(unstake_amount_1, PeriodType::Voting), + staking_info.unstake(unstake_amount_1, Subperiod::Voting), (unstake_amount_1, Balance::zero()) ); assert_eq!( @@ -1211,7 +1295,7 @@ fn singular_staking_info_unstake_during_voting_is_ok() { // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. let remaining_stake = staking_info.total_staked_amount(); assert_eq!( - staking_info.unstake(remaining_stake + 1, PeriodType::Voting), + staking_info.unstake(remaining_stake + 1, Subperiod::Voting), (remaining_stake, Balance::zero()) ); assert!(staking_info.total_staked_amount().is_zero()); @@ -1221,19 +1305,19 @@ fn singular_staking_info_unstake_during_voting_is_ok() { #[test] fn singular_staking_info_unstake_during_bep_is_ok() { let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); + let subperiod = Subperiod::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + staking_info.stake(vote_stake_amount_1, Subperiod::Voting); let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); // 1st scenario - Unstake some of the amount staked during B&E period let unstake_1 = 5; assert_eq!( - staking_info.unstake(5, PeriodType::BuildAndEarn), + staking_info.unstake(5, Subperiod::BuildAndEarn), (Balance::zero(), unstake_1) ); assert_eq!( @@ -1241,11 +1325,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::BuildAndEarn), + staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 - unstake_1 ); assert!(staking_info.is_loyal()); @@ -1253,12 +1337,12 @@ fn singular_staking_info_unstake_during_bep_is_ok() { // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. // The point is to take a chunk from the voting period stake too. let current_total_stake = staking_info.total_staked_amount(); - let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); + let current_bep_stake = staking_info.staked_amount(Subperiod::BuildAndEarn); let voting_stake_overflow = 2; let unstake_2 = current_bep_stake + voting_stake_overflow; assert_eq!( - staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), + staking_info.unstake(unstake_2, Subperiod::BuildAndEarn), (voting_stake_overflow, current_bep_stake) ); assert_eq!( @@ -1266,11 +1350,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { current_total_stake - unstake_2 ); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 - voting_stake_overflow ); assert!(staking_info - .staked_amount(PeriodType::BuildAndEarn) + .staked_amount(Subperiod::BuildAndEarn) .is_zero()); assert!( !staking_info.is_loyal(), @@ -1325,7 +1409,11 @@ fn contract_stake_info_stake_is_ok() { let era_1 = 3; let stake_era_1 = era_1 + 1; let period_1 = 5; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + ending_era: 20, + }; let amount_1 = 31; contract_stake.stake(amount_1, period_info_1, era_1); assert!(!contract_stake.is_empty()); @@ -1342,7 +1430,11 @@ fn contract_stake_info_stake_is_ok() { assert_eq!(entry_1_1.total(), amount_1); // 2nd scenario - stake some more to the same era but different period type, and verify state change. - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + ending_era: 20, + }; contract_stake.stake(amount_1, period_info_1, era_1); let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); assert_eq!(entry_1_2.era, stake_era_1); @@ -1368,7 +1460,11 @@ fn contract_stake_info_stake_is_ok() { let era_3 = era_2 + 3; let stake_era_3 = era_3 + 1; let period_2 = period_1 + 1; - let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); + let period_info_2 = PeriodInfo { + number: period_2, + subperiod: Subperiod::BuildAndEarn, + ending_era: 20, + }; let amount_3 = 41; contract_stake.stake(amount_3, period_info_2, era_3); @@ -1409,7 +1505,11 @@ fn contract_stake_info_unstake_is_ok() { // Prep action - create a stake entry let era_1 = 2; let period = 3; - let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); + let period_info = PeriodInfo { + number: period, + subperiod: Subperiod::Voting, + ending_era: 20, + }; let stake_amount = 100; contract_stake.stake(stake_amount, period_info, era_1); @@ -1421,12 +1521,16 @@ fn contract_stake_info_unstake_is_ok() { stake_amount - amount_1 ); assert_eq!( - contract_stake.staked_amount(period, PeriodType::Voting), + contract_stake.staked_amount(period, Subperiod::Voting), stake_amount - amount_1 ); // 2nd scenario - unstake in the future era, entries should be aligned to the current era - let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); + let period_info = PeriodInfo { + number: period, + subperiod: Subperiod::BuildAndEarn, + ending_era: 40, + }; let era_2 = era_1 + 3; let amount_2 = 7; contract_stake.unstake(amount_2, period_info, era_2); @@ -1435,7 +1539,7 @@ fn contract_stake_info_unstake_is_ok() { stake_amount - amount_1 - amount_2 ); assert_eq!( - contract_stake.staked_amount(period, PeriodType::Voting), + contract_stake.staked_amount(period, Subperiod::Voting), stake_amount - amount_1 - amount_2 ); } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index f8b0b264f8..287b4467cb 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -16,7 +16,12 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -// TODO: docs +//! # dApp Staking Module Types +//! +//! Contains various types, structs & enums used by the dApp staking implementation. +//! The main purpose of this is to abstract complexity away from the extrinsic call implementation, +//! and even more importantly to make the code more testable. +//! use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; @@ -39,6 +44,10 @@ pub type AccountLedgerFor = AccountLedger, ::M pub type DAppTierRewardsFor = DAppTierRewards, ::NumberOfTiers>; +// TODO: temp experimental type, don't review +pub type ContractEntriesFor = + ExperimentalContractStakeEntries, ::NumberOfTiers>; + // Helper struct for converting `u16` getter into `u32` #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct MaxNumberOfContractsU32(PhantomData); @@ -76,22 +85,21 @@ pub enum AccountLedgerError { AlreadyClaimed, } -// TODO: rename to SubperiodType? It would be less ambigious. -/// Distinct period types in dApp staking protocol. +/// Distinct subperiods in dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub enum PeriodType { - /// Period during which the focus is on voting. +pub enum Subperiod { + /// Subperiod during which the focus is on voting. Voting, - /// Period during which dApps and stakers earn rewards. + /// Subperiod during which dApps and stakers earn rewards. BuildAndEarn, } -impl PeriodType { - /// Next period type, after `self`. +impl Subperiod { + /// Next subperiod, after `self`. pub fn next(&self) -> Self { match self { - PeriodType::Voting => PeriodType::BuildAndEarn, - PeriodType::BuildAndEarn => PeriodType::Voting, + Subperiod::Voting => Subperiod::BuildAndEarn, + Subperiod::BuildAndEarn => Subperiod::Voting, } } } @@ -102,27 +110,18 @@ pub struct PeriodInfo { /// Period number. #[codec(compact)] pub number: PeriodNumber, - /// Subperiod type. - pub period_type: PeriodType, - /// Last ear of the sub-period, after this a new sub-period should start. + /// subperiod. + pub subperiod: Subperiod, + /// Last ear of the subperiod, after this a new subperiod should start. #[codec(compact)] pub ending_era: EraNumber, } impl PeriodInfo { - /// Create new instance of `PeriodInfo` - pub fn new(number: PeriodNumber, period_type: PeriodType, ending_era: EraNumber) -> Self { - Self { - number, - period_type, - ending_era, - } - } - /// `true` if the provided era belongs to the next period, `false` otherwise. - /// It's only possible to provide this information for the `BuildAndEarn` period type. + /// It's only possible to provide this information for the `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { - self.period_type == PeriodType::BuildAndEarn && self.ending_era <= era + self.subperiod == Subperiod::BuildAndEarn && self.ending_era <= era } } @@ -145,8 +144,8 @@ pub struct PeriodEndInfo { pub enum ForcingType { /// Force the next era to start. Era, - /// Force the current period type to end, and new one to start. It will also force a new era to start. - PeriodType, + /// Force the current subperiod to end, and new one to start. It will also force a new era to start. + Subperiod, } /// General information & state of the dApp staking protocol. @@ -174,7 +173,7 @@ where next_era_start: BlockNumber::from(1_u32), period_info: PeriodInfo { number: 0, - period_type: PeriodType::Voting, + subperiod: Subperiod::Voting, ending_era: 2, }, maintenance: false, @@ -186,9 +185,9 @@ impl ProtocolState where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, { - /// Current sub-period type. - pub fn period_type(&self) -> PeriodType { - self.period_info.period_type + /// Current subperiod. + pub fn subperiod(&self) -> Subperiod { + self.period_info.subperiod } /// Current period number. @@ -207,9 +206,9 @@ where self.next_era_start <= now } - /// Triggers the next period type, updating appropriate parameters. - pub fn into_next_period_type(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { - let period_number = if self.period_type() == PeriodType::BuildAndEarn { + /// Triggers the next subperiod, updating appropriate parameters. + pub fn into_next_subperiod(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { + let period_number = if self.subperiod() == Subperiod::BuildAndEarn { self.period_number().saturating_add(1) } else { self.period_number() @@ -217,7 +216,7 @@ where self.period_info = PeriodInfo { number: period_number, - period_type: self.period_type().next(), + subperiod: self.subperiod().next(), ending_era, }; self.next_era_start = next_era_start; @@ -315,6 +314,7 @@ pub struct AccountLedger< // TODO: introduce a variable which keeps track of the latest era for which the rewards have been calculated. // This is needed since in case we break up reward calculation into multiple blocks, we should prohibit staking until // reward calculation has finished. + // >>> Only if we decide to break calculation into multiple steps. } impl Default for AccountLedger @@ -468,15 +468,13 @@ where } } - /// How much is staked for the specified period type, in respect to the specified era. - pub fn staked_amount_for_type(&self, period_type: PeriodType, period: PeriodNumber) -> Balance { + /// How much is staked for the specified subperiod, in respect to the specified era. + pub fn staked_amount_for_type(&self, subperiod: Subperiod, period: PeriodNumber) -> Balance { // First check the 'future' entry, afterwards check the 'first' entry match self.staked_future { - Some(stake_amount) if stake_amount.period == period => { - stake_amount.for_type(period_type) - } + Some(stake_amount) if stake_amount.period == period => stake_amount.for_type(subperiod), _ => match self.staked { - stake_amount if stake_amount.period == period => stake_amount.for_type(period_type), + stake_amount if stake_amount.period == period => stake_amount.for_type(subperiod), _ => Balance::zero(), }, } @@ -540,13 +538,13 @@ where // Update existing entry if it exists, otherwise create it. match self.staked_future.as_mut() { Some(stake_amount) => { - stake_amount.add(amount, current_period_info.period_type); + stake_amount.add(amount, current_period_info.subperiod); } None => { let mut stake_amount = self.staked; stake_amount.era = era + 1; stake_amount.period = current_period_info.number; - stake_amount.add(amount, current_period_info.period_type); + stake_amount.add(amount, current_period_info.subperiod); self.staked_future = Some(stake_amount); } } @@ -576,8 +574,7 @@ where return Err(AccountLedgerError::UnstakeAmountLargerThanStake); } - self.staked - .subtract(amount, current_period_info.period_type); + self.staked.subtract(amount, current_period_info.subperiod); // Convenience cleanup if self.staked.is_empty() { @@ -585,7 +582,7 @@ where } if let Some(mut stake_amount) = self.staked_future { - stake_amount.subtract(amount, current_period_info.period_type); + stake_amount.subtract(amount, current_period_info.subperiod); self.staked_future = if stake_amount.is_empty() { None @@ -656,7 +653,7 @@ where // 1. We only have future entry, no current entry // 2. We have both current and future entry // 3. We only have current entry, no future entry - let (span, maybe_other) = if let Some(stake_amount) = self.staked_future { + let (span, maybe_first) = if let Some(stake_amount) = self.staked_future { if self.staked.is_empty() { ((stake_amount.era, era, stake_amount.total()), None) } else { @@ -669,7 +666,7 @@ where ((self.staked.era, era, self.staked.total()), None) }; - let result = EraStakePairIter::new(span, maybe_other); + let result = EraStakePairIter::new(span, maybe_first); // Rollover future to 'current' stake amount if let Some(stake_amount) = self.staked_future.take() { @@ -695,15 +692,15 @@ where /// Due to how `AccountLedger` is implemented, few scenarios are possible when claming rewards: /// /// 1. `staked` has some amount, `staked_future` is `None` -/// * `maybe_other` is `None`, span describes the entire range +/// * `maybe_first` is `None`, span describes the entire range /// 2. `staked` has nothing, `staked_future` is some and has some amount -/// * `maybe_other` is `None`, span describes the entire range +/// * `maybe_first` is `None`, span describes the entire range /// 3. `staked` has some amount, `staked_future` has some amount -/// * `maybe_other` is `Some` and covers the `staked` entry, span describes the entire range except the first pair. +/// * `maybe_first` is `Some` and covers the `staked` entry, span describes the entire range except the first pair. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct EraStakePairIter { /// Denotes whether the first entry is different than the others. - maybe_other: Option<(EraNumber, Balance)>, + maybe_first: Option<(EraNumber, Balance)>, /// Starting era of the span. start_era: EraNumber, /// Ending era of the span. @@ -716,9 +713,9 @@ impl EraStakePairIter { /// Create new iterator struct for `(era, staked amount)` pairs. pub fn new( span: (EraNumber, EraNumber, Balance), - maybe_other: Option<(EraNumber, Balance)>, + maybe_first: Option<(EraNumber, Balance)>, ) -> Self { - if let Some((era, _amount)) = maybe_other { + if let Some((era, _amount)) = maybe_first { debug_assert!( span.0 == era + 1, "The 'other', if it exists, must cover era preceding the span." @@ -726,7 +723,7 @@ impl EraStakePairIter { } Self { - maybe_other, + maybe_first, start_era: span.0, end_era: span.1, amount: span.2, @@ -739,7 +736,7 @@ impl Iterator for EraStakePairIter { fn next(&mut self) -> Option { // Fist cover the scenario where we have a unique first value - if let Some((era, amount)) = self.maybe_other.take() { + if let Some((era, amount)) = self.maybe_first.take() { return Some((era, amount)); } @@ -792,37 +789,37 @@ impl StakeAmount { self.voting.is_zero() && self.build_and_earn.is_zero() } - /// Total amount staked in both period types. + /// Total amount staked in both subperiods. pub fn total(&self) -> Balance { self.voting.saturating_add(self.build_and_earn) } - /// Amount staked for the specified period type. - pub fn for_type(&self, period_type: PeriodType) -> Balance { - match period_type { - PeriodType::Voting => self.voting, - PeriodType::BuildAndEarn => self.build_and_earn, + /// Amount staked for the specified subperiod. + pub fn for_type(&self, subperiod: Subperiod) -> Balance { + match subperiod { + Subperiod::Voting => self.voting, + Subperiod::BuildAndEarn => self.build_and_earn, } } - /// Stake the specified `amount` for the specified `period_type`. - pub fn add(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.voting.saturating_accrue(amount), - PeriodType::BuildAndEarn => self.build_and_earn.saturating_accrue(amount), + /// Stake the specified `amount` for the specified `subperiod`. + pub fn add(&mut self, amount: Balance, subperiod: Subperiod) { + match subperiod { + Subperiod::Voting => self.voting.saturating_accrue(amount), + Subperiod::BuildAndEarn => self.build_and_earn.saturating_accrue(amount), } } - /// Unstake the specified `amount` for the specified `period_type`. + /// Unstake the specified `amount` for the specified `subperiod`. /// - /// In case period type is `Voting`, the amount is subtracted from the voting period. + /// In case subperiod is `Voting`, the amount is subtracted from the voting period. /// - /// In case period type is `Build&Earn`, the amount is first subtracted from the + /// In case subperiod is `Build&Earn`, the amount is first subtracted from the /// build&earn amount, and any rollover is subtracted from the voting period. - pub fn subtract(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.voting.saturating_reduce(amount), - PeriodType::BuildAndEarn => { + pub fn subtract(&mut self, amount: Balance, subperiod: Subperiod) { + match subperiod { + Subperiod::Voting => self.voting.saturating_reduce(amount), + Subperiod::BuildAndEarn => { if self.build_and_earn >= amount { self.build_and_earn.saturating_reduce(amount); } else { @@ -875,15 +872,15 @@ impl EraInfo { self.unlocking.saturating_reduce(amount); } - /// Add the specified `amount` to the appropriate stake amount, based on the `PeriodType`. - pub fn add_stake_amount(&mut self, amount: Balance, period_type: PeriodType) { - self.next_stake_amount.add(amount, period_type); + /// Add the specified `amount` to the appropriate stake amount, based on the `Subperiod`. + pub fn add_stake_amount(&mut self, amount: Balance, subperiod: Subperiod) { + self.next_stake_amount.add(amount, subperiod); } - /// Subtract the specified `amount` from the appropriate stake amount, based on the `PeriodType`. - pub fn unstake_amount(&mut self, amount: Balance, period_type: PeriodType) { - self.current_stake_amount.subtract(amount, period_type); - self.next_stake_amount.subtract(amount, period_type); + /// Subtract the specified `amount` from the appropriate stake amount, based on the `Subperiod`. + pub fn unstake_amount(&mut self, amount: Balance, subperiod: Subperiod) { + self.current_stake_amount.subtract(amount, subperiod); + self.next_stake_amount.subtract(amount, subperiod); } /// Total staked amount in this era. @@ -892,8 +889,8 @@ impl EraInfo { } /// Staked amount of specified `type` in this era. - pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - self.current_stake_amount.for_type(period_type) + pub fn staked_amount(&self, subperiod: Subperiod) -> Balance { + self.current_stake_amount.for_type(subperiod) } /// Total staked amount in the next era. @@ -902,23 +899,23 @@ impl EraInfo { } /// Staked amount of specifeid `type` in the next era. - pub fn staked_amount_next_era(&self, period_type: PeriodType) -> Balance { - self.next_stake_amount.for_type(period_type) + pub fn staked_amount_next_era(&self, subperiod: Subperiod) -> Balance { + self.next_stake_amount.for_type(subperiod) } /// Updates `Self` to reflect the transition to the next era. /// /// ## Args - /// `next_period_type` - `None` if no period type change, `Some(type)` if `type` is starting from the next era. - pub fn migrate_to_next_era(&mut self, next_period_type: Option) { + /// `next_subperiod` - `None` if no subperiod change, `Some(type)` if `type` is starting from the next era. + pub fn migrate_to_next_era(&mut self, next_subperiod: Option) { self.active_era_locked = self.total_locked; - match next_period_type { + match next_subperiod { // If next era marks start of new voting period period, it means we're entering a new period - Some(PeriodType::Voting) => { + Some(Subperiod::Voting) => { self.current_stake_amount = Default::default(); self.next_stake_amount = Default::default(); } - Some(PeriodType::BuildAndEarn) | None => { + Some(Subperiod::BuildAndEarn) | None => { self.current_stake_amount = self.next_stake_amount; } }; @@ -942,19 +939,18 @@ impl SingularStakingInfo { /// ## Args /// /// `period` - period number for which this entry is relevant. - /// `period_type` - period type during which this entry is created. - pub fn new(period: PeriodNumber, period_type: PeriodType) -> Self { + /// `subperiod` - subperiod during which this entry is created. + pub fn new(period: PeriodNumber, subperiod: Subperiod) -> Self { Self { - // TODO: one drawback here is using the struct which has `era` as the field - it's not needed here. Should I add a special struct just for this? staked: StakeAmount::new(Balance::zero(), Balance::zero(), 0, period), // Loyalty staking is only possible if stake is first made during the voting period. - loyal_staker: period_type == PeriodType::Voting, + loyal_staker: subperiod == Subperiod::Voting, } } - /// Stake the specified amount on the contract, for the specified period type. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { - self.staked.add(amount, period_type); + /// Stake the specified amount on the contract, for the specified subperiod. + pub fn stake(&mut self, amount: Balance, subperiod: Subperiod) { + self.staked.add(amount, subperiod); } /// Unstakes some of the specified amount from the contract. @@ -963,15 +959,14 @@ impl SingularStakingInfo { /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. /// /// Returns the amount that was unstaked from the `voting period` stake, and from the `build&earn period` stake. - pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) -> (Balance, Balance) { + pub fn unstake(&mut self, amount: Balance, subperiod: Subperiod) -> (Balance, Balance) { let snapshot = self.staked; - self.staked.subtract(amount, period_type); + self.staked.subtract(amount, subperiod); self.loyal_staker = self.loyal_staker - && (period_type == PeriodType::Voting - || period_type == PeriodType::BuildAndEarn - && self.staked.voting == snapshot.voting); + && (subperiod == Subperiod::Voting + || subperiod == Subperiod::BuildAndEarn && self.staked.voting == snapshot.voting); // Amount that was unstaked ( @@ -982,14 +977,14 @@ impl SingularStakingInfo { ) } - /// Total staked on the contract by the user. Both period type stakes are included. + /// Total staked on the contract by the user. Both subperiod stakes are included. pub fn total_staked_amount(&self) -> Balance { self.staked.total() } /// Returns amount staked in the specified period. - pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - self.staked.for_type(period_type) + pub fn staked_amount(&self, subperiod: Subperiod) -> Balance { + self.staked.for_type(subperiod) } /// If `true` staker has staked during voting period and has never reduced their sta @@ -1083,18 +1078,18 @@ impl ContractStakeAmount { } } - /// Staked amount on the contract, for specified period type, in the active period. - pub fn staked_amount(&self, active_period: PeriodNumber, period_type: PeriodType) -> Balance { + /// Staked amount on the contract, for specified subperiod, in the active period. + pub fn staked_amount(&self, active_period: PeriodNumber, subperiod: Subperiod) -> Balance { match (self.staked, self.staked_future) { (_, Some(staked_future)) if staked_future.period == active_period => { - staked_future.for_type(period_type) + staked_future.for_type(subperiod) } - (staked, _) if staked.period == active_period => staked.for_type(period_type), + (staked, _) if staked.period == active_period => staked.for_type(subperiod), _ => Balance::zero(), } } - /// Stake the specified `amount` on the contract, for the specified `period_type` and `era`. + /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`. pub fn stake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { let stake_era = current_era.saturating_add(1); // TODO: maybe keep the check that period/era aren't historical? @@ -1103,7 +1098,7 @@ impl ContractStakeAmount { match self.staked_future.as_mut() { // Future entry matches the era, just updated it and return Some(stake_amount) if stake_amount.era == stake_era => { - stake_amount.add(amount, period_info.period_type); + stake_amount.add(amount, period_info.subperiod); return; } // Future entry has older era, but periods match so overwrite the 'current' entry with it @@ -1121,7 +1116,7 @@ impl ContractStakeAmount { // otherwise just create a dummy new entry _ => Default::default(), }; - new_entry.add(amount, period_info.period_type); + new_entry.add(amount, period_info.subperiod); new_entry.era = stake_era; new_entry.period = period_info.number; @@ -1133,7 +1128,7 @@ impl ContractStakeAmount { } } - /// Unstake the specified `amount` from the contract, for the specified `period_type` and `era`. + /// Unstake the specified `amount` from the contract, for the specified `subperiod` and `era`. pub fn unstake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { // TODO: tests need to be re-writen for this after the refactoring @@ -1156,9 +1151,9 @@ impl ContractStakeAmount { } // Subtract both amounts - self.staked.subtract(amount, period_info.period_type); + self.staked.subtract(amount, period_info.subperiod); if let Some(stake_amount) = self.staked_future.as_mut() { - stake_amount.subtract(amount, period_info.period_type); + stake_amount.subtract(amount, period_info.subperiod); } // Conevnience cleanup @@ -1657,3 +1652,36 @@ pub trait RewardPoolProvider { /// Get the bonus pool for stakers. fn bonus_reward_pool() -> Balance; } + +// TODO: this is experimental, don't review +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub struct ExperimentalContractStakeEntry { + #[codec(compact)] + pub dapp_id: DAppId, + #[codec(compact)] + pub voting: Balance, + #[codec(compact)] + pub build_and_earn: Balance, +} + +// TODO: this is experimental, don't review +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(MD, NT))] +pub struct ExperimentalContractStakeEntries, 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, + /// Period during which this struct was created. + #[codec(compact)] + pub period: PeriodNumber, +} From d0d196e147a0b10507804106eb74d7368fe72a1c Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 2 Nov 2023 18:28:03 +0100 Subject: [PATCH 45/86] Readme, docs --- pallets/dapp-staking-v3/README.md | 51 ++++++++++++++++++++++++++++ pallets/dapp-staking-v3/src/types.rs | 11 +++--- 2 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 pallets/dapp-staking-v3/README.md diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md new file mode 100644 index 0000000000..9022056568 --- /dev/null +++ b/pallets/dapp-staking-v3/README.md @@ -0,0 +1,51 @@ +# dApp Staking v3 + +## Introduction + +Astar and Shiden networks provide a unique way for developers to earn rewards by developing products that native token holdes decide to support. + +The principle is simple - stakers lock their tokens to _stake_ on a dApp, and if the dApp attracts enough support, it is rewarded in native currency, derived from the inflation. +In turn stakers are rewarded for locking & staking their tokens. + +## Functionality Overview + +### Eras + +Eras are the basic _time unit_ in dApp staking and their length is measured in the number of blocks. + +They are not expected to last long, e.g. current live networks era length is roughly 1 day (7200 blocks). +After an era ends, it's usually possible to claim rewards for it, if user or dApp are eligible. + +### Periods + +Periods are another _time unit_ in dApp staking. They are expected to be more lengthy than eras. + +Each period consists of two subperiods: +* `Voting` +* `Build&Earn` + +Each period is denoted by a number, which increments each time a new period begins. +Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. + +#### Voting + +When `Voting` starts, all _stakes_ are reset to **zero**. +Projects participating in dApp staking are expected to market themselves to (re)attract stakers. + +Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it. +Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate. + +`Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting period is treated as a single _voting era_. +E.g. if `voting` subperiod lasts for **10 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **1000** blocks. + +Neither stakers nor dApps earn rewards during this subperiod - no new rewards are generated after `voting` subperiod ends. + +#### Build&Earn + +`Build&Earn` subperiod consits of one or more eras, therefore its length is expressed in eras. + +After each _era_ end, eligible stakers and dApps can claim the rewards they earned. Rewards are only claimable for the finished eras. + +It is still possible to _stake_ during this period, and stakers are encouraged to do so since this will increase the rewards they earn. +The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era). + diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 287b4467cb..ecef35017a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -22,6 +22,11 @@ //! The main purpose of this is to abstract complexity away from the extrinsic call implementation, //! and even more importantly to make the code more testable. //! +//! # Overview +//! +//! ## Protocol State +//! +//! * `Subperiod` - an enum describing which subperiod is active in the current period. use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; @@ -1091,9 +1096,8 @@ impl ContractStakeAmount { /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`. pub fn stake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { + // TODO: tests need to be re-writen for this after the refactoring let stake_era = current_era.saturating_add(1); - // TODO: maybe keep the check that period/era aren't historical? - // TODO2: tests need to be re-writen for this after the refactoring match self.staked_future.as_mut() { // Future entry matches the era, just updated it and return @@ -1345,7 +1349,6 @@ impl> TierParameters { number_of_tiers == self.reward_portion.len() && number_of_tiers == self.slot_distribution.len() && number_of_tiers == self.tier_thresholds.len() - // TODO: Make check more detailed, verify that entries sum up to 1 or 100% } } @@ -1360,8 +1363,6 @@ 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, From 9723d9f0f5d9fc65c3a370b740e827ca8e749875 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 3 Nov 2023 11:58:06 +0100 Subject: [PATCH 46/86] Minor renaming, docs --- pallets/dapp-staking-v3/src/benchmarking.rs | 2 +- pallets/dapp-staking-v3/src/lib.rs | 13 +-- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- pallets/dapp-staking-v3/src/test/tests.rs | 9 +- .../dapp-staking-v3/src/test/tests_types.rs | 84 ++++++++++--------- pallets/dapp-staking-v3/src/types.rs | 57 +++++++++++-- 6 files changed, 109 insertions(+), 58 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index ac0ac838c7..e98fd1ce09 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -100,7 +100,7 @@ pub fn initial_config() { period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - ending_era: 2, + subperiod_end_era: 2, }, maintenance: false, }); diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8f9797bb9f..654c53b28d 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -479,7 +479,7 @@ pub mod pallet { period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - ending_era: 2, + subperiod_end_era: 2, }, maintenance: false, }; @@ -524,11 +524,12 @@ pub mod pallet { dapp_reward_pool: Balance::zero(), }; - let ending_era = + let subperiod_end_era = next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.into_next_subperiod(ending_era, build_and_earn_start_block); + protocol_state + .into_next_subperiod(subperiod_end_era, build_and_earn_start_block); era_info.migrate_to_next_era(Some(protocol_state.subperiod())); @@ -576,11 +577,11 @@ pub mod pallet { // For the sake of consistency we treat the whole `Voting` period as a single era. // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. - let ending_era = next_era.saturating_add(1); + let subperiod_end_era = next_era.saturating_add(1); let voting_period_length = Self::blocks_per_voting_period(); let next_era_start_block = now.saturating_add(voting_period_length); - protocol_state.into_next_subperiod(ending_era, next_era_start_block); + protocol_state.into_next_subperiod(subperiod_end_era, next_era_start_block); era_info.migrate_to_next_era(Some(protocol_state.subperiod())); @@ -1522,7 +1523,7 @@ pub mod pallet { match force_type { ForcingType::Era => (), ForcingType::Subperiod => { - state.period_info.ending_era = state.era.saturating_add(1); + state.period_info.subperiod_end_era = state.era.saturating_add(1); } } }); diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index a2fdd20a26..f186f7e5a6 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -205,7 +205,7 @@ impl ExtBuilder { period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - ending_era: 2, + subperiod_end_era: 2, }, maintenance: false, }); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index acc842314e..90de8bbb7c 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -870,7 +870,7 @@ fn stake_in_final_era_fails() { // Force Build&Earn period ActiveProtocolState::::mutate(|state| { state.period_info.subperiod = Subperiod::BuildAndEarn; - state.period_info.ending_era = state.era + 1; + state.period_info.subperiod_end_era = state.era + 1; }); // Try to stake in the final era of the period, which should fail. @@ -1293,7 +1293,12 @@ fn claim_staker_rewards_after_expiry_fails() { ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); advance_to_into_next_subperiod(); - advance_to_era(ActiveProtocolState::::get().period_info.ending_era - 1); + advance_to_era( + ActiveProtocolState::::get() + .period_info + .subperiod_end_era + - 1, + ); assert_claim_staker_rewards(account); // Ensure we're still in the first period for the sake of test validity diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index a3d3e7d5b9..71f13e2bdd 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -45,23 +45,27 @@ fn subperiod_sanity_check() { #[test] fn period_info_basic_checks() { let period_number = 2; - let ending_era = 5; + let subperiod_end_era = 5; let info = PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - ending_era: ending_era, + subperiod_end_era: subperiod_end_era, }; // Sanity checks assert_eq!(info.number, period_number); assert_eq!(info.subperiod, Subperiod::Voting); - assert_eq!(info.ending_era, ending_era); + assert_eq!(info.subperiod_end_era, subperiod_end_era); // Voting period checks - assert!(!info.is_next_period(ending_era - 1)); - assert!(!info.is_next_period(ending_era)); - assert!(!info.is_next_period(ending_era + 1)); - for era in vec![ending_era - 1, ending_era, ending_era + 1] { + assert!(!info.is_next_period(subperiod_end_era - 1)); + assert!(!info.is_next_period(subperiod_end_era)); + assert!(!info.is_next_period(subperiod_end_era + 1)); + for era in vec![ + subperiod_end_era - 1, + subperiod_end_era, + subperiod_end_era + 1, + ] { assert!( !info.is_next_period(era), "Cannot trigger 'true' in the Voting period type." @@ -72,11 +76,11 @@ fn period_info_basic_checks() { let info = PeriodInfo { number: period_number, subperiod: Subperiod::BuildAndEarn, - ending_era: ending_era, + subperiod_end_era: subperiod_end_era, }; - assert!(!info.is_next_period(ending_era - 1)); - assert!(info.is_next_period(ending_era)); - assert!(info.is_next_period(ending_era + 1)); + assert!(!info.is_next_period(subperiod_end_era - 1)); + assert!(info.is_next_period(subperiod_end_era)); + assert!(info.is_next_period(subperiod_end_era + 1)); } #[test] @@ -94,12 +98,12 @@ fn protocol_state_default() { fn protocol_state_basic_checks() { let mut protocol_state = ProtocolState::::default(); let period_number = 5; - let ending_era = 11; + let subperiod_end_era = 11; let next_era_start = 31; protocol_state.period_info = PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - ending_era: ending_era, + subperiod_end_era: subperiod_end_era, }; protocol_state.next_era_start = next_era_start; @@ -112,30 +116,30 @@ fn protocol_state_basic_checks() { assert!(protocol_state.is_new_era(next_era_start + 1)); // Toggle new period type check - 'Voting' to 'BuildAndEarn' - let ending_era_1 = 23; + let subperiod_end_era_1 = 23; let next_era_start_1 = 41; - protocol_state.into_next_subperiod(ending_era_1, next_era_start_1); + protocol_state.into_next_subperiod(subperiod_end_era_1, next_era_start_1); assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!( protocol_state.period_number(), period_number, "Switching from 'Voting' to 'BuildAndEarn' should not trigger period bump." ); - assert_eq!(protocol_state.period_end_era(), ending_era_1); + assert_eq!(protocol_state.period_end_era(), subperiod_end_era_1); assert!(!protocol_state.is_new_era(next_era_start_1 - 1)); assert!(protocol_state.is_new_era(next_era_start_1)); // Toggle from 'BuildAndEarn' over to 'Voting' - let ending_era_2 = 24; + let subperiod_end_era_2 = 24; let next_era_start_2 = 91; - protocol_state.into_next_subperiod(ending_era_2, next_era_start_2); + protocol_state.into_next_subperiod(subperiod_end_era_2, next_era_start_2); assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!( protocol_state.period_number(), period_number + 1, "Switching from 'BuildAndEarn' to 'Voting' must trigger period bump." ); - assert_eq!(protocol_state.period_end_era(), ending_era_2); + assert_eq!(protocol_state.period_end_era(), subperiod_end_era_2); assert!(protocol_state.is_new_era(next_era_start_2)); } @@ -481,7 +485,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - ending_era: 0 + subperiod_end_era: 0 } ) .is_ok()); @@ -494,7 +498,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - ending_era: 100, + subperiod_end_era: 100, }; let lock_amount = 17; let stake_amount = 11; @@ -531,7 +535,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { let period_info_2 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - ending_era: 100, + subperiod_end_era: 100, }; assert!(acc_ledger .add_stake_amount(1, first_era, period_info_2) @@ -559,7 +563,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - ending_era: 100, + subperiod_end_era: 100, }; let lock_amount = 17; let stake_amount_1 = 11; @@ -606,7 +610,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - ending_era: 100, + subperiod_end_era: 100, }; let lock_amount = 13; let stake_amount = 7; @@ -629,7 +633,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - ending_era: 100 + subperiod_end_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -650,7 +654,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - ending_era: 100 + subperiod_end_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -670,7 +674,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { PeriodInfo { number: 1, subperiod: Subperiod::Voting, - ending_era: 100 + subperiod_end_era: 100 } ), Err(AccountLedgerError::UnavailableStakeFunds) @@ -682,7 +686,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - ending_era: 100, + subperiod_end_era: 100, }; let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); @@ -713,7 +717,7 @@ fn account_ledger_unstake_amount_basic_scenario_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - ending_era: 100, + subperiod_end_era: 100, }; acc_ledger.add_lock_amount(amount_1); @@ -764,7 +768,7 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - ending_era: 100, + subperiod_end_era: 100, }; acc_ledger.add_lock_amount(amount_1); @@ -836,7 +840,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - ending_era: 100, + subperiod_end_era: 100, }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger @@ -857,7 +861,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - ending_era: 100 + subperiod_end_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -878,7 +882,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - ending_era: 100 + subperiod_end_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -897,7 +901,7 @@ fn account_ledger_unstake_too_much_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - ending_era: 100, + subperiod_end_era: 100, }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger @@ -930,7 +934,7 @@ fn account_ledger_unlockable_amount_works() { let period_info = PeriodInfo { number: stake_period, subperiod: Subperiod::Voting, - ending_era: 100, + subperiod_end_era: 100, }; assert!(acc_ledger .add_stake_amount(stake_amount, lock_era, period_info) @@ -1412,7 +1416,7 @@ fn contract_stake_info_stake_is_ok() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - ending_era: 20, + subperiod_end_era: 20, }; let amount_1 = 31; contract_stake.stake(amount_1, period_info_1, era_1); @@ -1433,7 +1437,7 @@ fn contract_stake_info_stake_is_ok() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - ending_era: 20, + subperiod_end_era: 20, }; contract_stake.stake(amount_1, period_info_1, era_1); let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); @@ -1463,7 +1467,7 @@ fn contract_stake_info_stake_is_ok() { let period_info_2 = PeriodInfo { number: period_2, subperiod: Subperiod::BuildAndEarn, - ending_era: 20, + subperiod_end_era: 20, }; let amount_3 = 41; @@ -1508,7 +1512,7 @@ fn contract_stake_info_unstake_is_ok() { let period_info = PeriodInfo { number: period, subperiod: Subperiod::Voting, - ending_era: 20, + subperiod_end_era: 20, }; let stake_amount = 100; contract_stake.stake(stake_amount, period_info, era_1); @@ -1529,7 +1533,7 @@ fn contract_stake_info_unstake_is_ok() { let period_info = PeriodInfo { number: period, subperiod: Subperiod::BuildAndEarn, - ending_era: 40, + subperiod_end_era: 40, }; let era_2 = era_1 + 3; let amount_2 = 7; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index ecef35017a..61a45152a9 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -24,9 +24,46 @@ //! //! # Overview //! -//! ## Protocol State +//! The following is a high level overview of the implemented structs, enums & types. +//! For detailes, please refer to the documentation and code of each individual type. //! +//! ## General Protocol Information +//! +//! * `EraNumber` - numeric Id of an era. +//! * `PeriodNumber` - numeric Id of a period. //! * `Subperiod` - an enum describing which subperiod is active in the current period. +//! * `PeriodInfo` - contains information about the ongoing period, like period number, current subperiod and when will the current subperiod end. +//! * `PeriodEndInfo` - contains information about a finished past period, like the final era of the period, total amount staked & bonus reward pool. +//! * `ProtocolState` - contains the most general protocol state info: current era number, block when the era ends, ongoing period info, and whether protocol is in maintenance mode. +//! +//! ## DApp Information +//! +//! * `DAppId` - a compact unique numeric Id of a dApp. +//! * `DAppInfo` - contains general information about a dApp, like owner and reward beneficiary, Id and state. +//! * `ContractStakeAmount` - contains information about how much is staked on a particular contract. +//! +//! ## Staker Information +//! +//! * `UnlockingChunk` - describes some amount undergoing the unlocking process. +//! * `StakeAmount` - contains information about the staked amount in a particular era, and period. +//! * `AccountLedger` - keeps track of total locked & staked balance, unlocking chunks and number of stake entries. +//! * `SingularStakingInfo` - contains information about a particular stakers stake on a specific smart contract. Used to track loyalty. +//! +//! ## Era Information +//! +//! * `EraInfo` - contains information about the ongoing era, like how much is locked & staked. +//! * `EraReward` - contains information about a finished era, like reward pools and total staked amount. +//! * `EraRewardSpan` - a composite of multiple `EraReward` objects, used to describe a range of finished eras. +//! +//! ## Tier Information +//! +//! * `TierThreshold` - an enum describing tier entry thresholds. +//! * `TierParameters` - contains static information about tiers, like init thresholds, reward & slot distribution. +//! * `TiersConfiguration` - contains dynamic information about tiers, derived from `TierParameters` and onchain data. +//! * `DAppTier` - a compact struct describing a dApp's tier. +//! * `DAppTierREwards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. +//! +//! TODO: some types are missing so double check before final merge that everything is covered and explained correctly use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; @@ -119,14 +156,14 @@ pub struct PeriodInfo { pub subperiod: Subperiod, /// Last ear of the subperiod, after this a new subperiod should start. #[codec(compact)] - pub ending_era: EraNumber, + pub subperiod_end_era: EraNumber, } impl PeriodInfo { /// `true` if the provided era belongs to the next period, `false` otherwise. /// It's only possible to provide this information for the `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { - self.subperiod == Subperiod::BuildAndEarn && self.ending_era <= era + self.subperiod == Subperiod::BuildAndEarn && self.subperiod_end_era <= era } } @@ -179,7 +216,7 @@ where period_info: PeriodInfo { number: 0, subperiod: Subperiod::Voting, - ending_era: 2, + subperiod_end_era: 2, }, maintenance: false, } @@ -202,7 +239,7 @@ where /// Ending era of current period pub fn period_end_era(&self) -> EraNumber { - self.period_info.ending_era + self.period_info.subperiod_end_era } /// Checks whether a new era should be triggered, based on the provided `BlockNumber` argument @@ -212,7 +249,11 @@ where } /// Triggers the next subperiod, updating appropriate parameters. - pub fn into_next_subperiod(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { + pub fn into_next_subperiod( + &mut self, + subperiod_end_era: EraNumber, + next_era_start: BlockNumber, + ) { let period_number = if self.subperiod() == Subperiod::BuildAndEarn { self.period_number().saturating_add(1) } else { @@ -222,7 +263,7 @@ where self.period_info = PeriodInfo { number: period_number, subperiod: self.subperiod().next(), - ending_era, + subperiod_end_era, }; self.next_era_start = next_era_start; } @@ -681,7 +722,7 @@ where // Make sure to clean up the entries if all rewards for the period have been claimed. match period_end { - Some(ending_era) if era >= ending_era => { + Some(subperiod_end_era) if era >= subperiod_end_era => { self.staked = Default::default(); self.staked_future = None; } From 24b4b53b22ff9d588083efeb7c3f710703ce82ce Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 3 Nov 2023 15:59:44 +0100 Subject: [PATCH 47/86] More docs --- pallets/dapp-staking-v3/README.md | 59 +++++++++++++++++++++++++++- pallets/dapp-staking-v3/src/lib.rs | 3 -- pallets/dapp-staking-v3/src/types.rs | 15 +++---- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index 9022056568..98a68c5be5 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -44,8 +44,63 @@ Neither stakers nor dApps earn rewards during this subperiod - no new rewards ar `Build&Earn` subperiod consits of one or more eras, therefore its length is expressed in eras. -After each _era_ end, eligible stakers and dApps can claim the rewards they earned. Rewards are only claimable for the finished eras. +After each _era_ ends, eligible stakers and dApps can claim the rewards they earned. Rewards are **only** claimable for the finished eras. It is still possible to _stake_ during this period, and stakers are encouraged to do so since this will increase the rewards they earn. -The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era). +The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era which would be in the next period). +### dApps & Smart Contracts + +Protocol is called dApp staking, but internally it essentially works with smart contracts, or even more precise, smart contract addresses. + +Throughout the code, when addressing a particular dApp, it's addressed as `smart contract`. Naming of the types & storage more closely follows `dApp` nomenclature. + +#### Registration + +Projects, or _dApps_, must be registered into protocol to participate. +Only a privileged `ManagerOrigin` can perform dApp registration. +The pallet itself does not make assumptions who the privileged origin is, and it can differ from runtime to runtime. + +There is a limit of how many smart contracts can be registered at once. Once the limit is reached, any additional attempt to register a new contract will fail. + +#### Reward Beneficiary & Ownership + +When contract is registered, it is assigned a unique compact numeric Id - 16 bit unsigned integer. This is important for the inner workings of the pallet, and is not directly exposed to the users. + +After a dApp has been registered, it is possible to modify reward beneficiary or even the owner of the dApp. The owner can perform reward delegation and can further transfer ownership. + +#### Unregistration + +dApp can be removed from the procotol by unregistering it. +This is a privileged action that only `ManagerOrigin` can perform. + +After a dApp has been unregistered, it's no longer eligible to receive rewards. +It's still possible however to claim past unclaimed rewards. + +Important to note that even if dApp has been unregistered, it still occupies a _slot_ +in the dApp staking protocol and counts towards maximum number of registered dApps. +This will be improved in the future when dApp data will be cleaned up after the period ends. + +### Stakers + +#### Locking Funds + +In order for users to participate in dApp staking, the first step they need to take is lock some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). + +**NOTE:** Locked funds cannot be used for paying fees, or for transfer. + +In order to participate, user must have a `MinimumLockedAmount` of native currency locked. This doesn't mean that they cannot lock _less_ in a single call, but total locked amount must always be equal or greater than `MinimumLockedAmount`. + +In case amount specified for locking is greater than what user has available, only what's available will be locked. + +#### Unlocking Funds + +User can at any time decide to unlock their tokens. However, it's not possible to unlock tokens which are staked, so user has to unstake them first. + +Once _unlock_ is successfully executed, the tokens aren't immediately unlocked, but instead must undergo the unlocking process. Once unlocking process has finished, user can _claim_ their unlocked tokens into their free balance. + +There is a limited number of `unlocking chunks` a user can have at any point in time. If limit is reached, user must claim existing unlocked chunks, or wait for them to be unlocked before claiming them to free up space for new chunks. + +In case calling unlocking some amount would take the user below the `MinimumLockedAmount`, **everything** will be unlocked. + +For users who decide they would rather re-lock their tokens then wait for the unlocking process to finish, there's an option to do so. All currently unlocking chunks are consumed, and added back into locked amount. \ No newline at end of file diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 654c53b28d..cd63dff29e 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -819,9 +819,6 @@ pub mod pallet { /// /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. - /// - /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, - /// caller should claim pending rewards, before retrying to lock additional funds. #[pallet::call_index(5)] #[pallet::weight(Weight::zero())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 61a45152a9..8f48239666 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -86,10 +86,6 @@ pub type AccountLedgerFor = AccountLedger, ::M pub type DAppTierRewardsFor = DAppTierRewards, ::NumberOfTiers>; -// TODO: temp experimental type, don't review -pub type ContractEntriesFor = - ExperimentalContractStakeEntries, ::NumberOfTiers>; - // Helper struct for converting `u16` getter into `u32` #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct MaxNumberOfContractsU32(PhantomData); @@ -357,10 +353,6 @@ pub struct AccountLedger< /// Number of contract stake entries in storage. #[codec(compact)] pub contract_stake_count: u32, - // TODO: introduce a variable which keeps track of the latest era for which the rewards have been calculated. - // This is needed since in case we break up reward calculation into multiple blocks, we should prohibit staking until - // reward calculation has finished. - // >>> Only if we decide to break calculation into multiple steps. } impl Default for AccountLedger @@ -1695,7 +1687,7 @@ pub trait RewardPoolProvider { fn bonus_reward_pool() -> Balance; } -// TODO: this is experimental, don't review +// TODO: these are experimental, don't review #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] pub struct ExperimentalContractStakeEntry { #[codec(compact)] @@ -1706,7 +1698,6 @@ pub struct ExperimentalContractStakeEntry { pub build_and_earn: Balance, } -// TODO: this is experimental, don't review #[derive( Encode, Decode, @@ -1727,3 +1718,7 @@ pub struct ExperimentalContractStakeEntries, NT: Get> { #[codec(compact)] pub period: PeriodNumber, } + +// TODO: temp experimental type, don't review +pub type ContractEntriesFor = + ExperimentalContractStakeEntries, ::NumberOfTiers>; From 408eaadf84db5796f7f0ea50418558891fce3a19 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 3 Nov 2023 17:18:03 +0100 Subject: [PATCH 48/86] More docs --- pallets/dapp-staking-v3/README.md | 67 +++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index 98a68c5be5..68cd444b1c 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -27,6 +27,8 @@ Each period consists of two subperiods: Each period is denoted by a number, which increments each time a new period begins. Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. +Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to zero. This helps prevent projects remaining staked due to intertia. + #### Voting When `Voting` starts, all _stakes_ are reset to **zero**. @@ -83,7 +85,7 @@ This will be improved in the future when dApp data will be cleaned up after the ### Stakers -#### Locking Funds +#### Locking Tokens In order for users to participate in dApp staking, the first step they need to take is lock some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). @@ -93,7 +95,7 @@ In order to participate, user must have a `MinimumLockedAmount` of native curren In case amount specified for locking is greater than what user has available, only what's available will be locked. -#### Unlocking Funds +#### Unlocking Tokens User can at any time decide to unlock their tokens. However, it's not possible to unlock tokens which are staked, so user has to unstake them first. @@ -103,4 +105,63 @@ There is a limited number of `unlocking chunks` a user can have at any point in In case calling unlocking some amount would take the user below the `MinimumLockedAmount`, **everything** will be unlocked. -For users who decide they would rather re-lock their tokens then wait for the unlocking process to finish, there's an option to do so. All currently unlocking chunks are consumed, and added back into locked amount. \ No newline at end of file +For users who decide they would rather re-lock their tokens then wait for the unlocking process to finish, there's an option to do so. All currently unlocking chunks are consumed, and added back into locked amount. + +#### Staking Tokens + +Locked tokens, which aren't being used for staking, can be used to stake on a dApp. This translates to _voting_ or _nominating_ a dApp to receive rewards derived from the inflation. User can stake on multiple dApps if they want to. + +The staked amount **must be precise**, no adjustment will be made by the pallet in case a too large amount is specified. + +The staked amount is only eligible for rewards from the next era - in other words, only the amount that has been staked for the entire era is eligible to receive rewards. + +It is not possible to stake if there are unclaimed rewards from past eras. User must ensure to first claim their pending rewards, before staking. This is also beneficial to the users since it allows them to lock & stake the earned rewards as well. + +User's stake on a contract must be equal or greater than the `MinimumStakeAmount`. This is similar to the minimum lock amount, but this limit is per contract. + +Although user can stake on multiple smart contracts, the amount is limited. To be more precise, amount of database entries that can exist per user is limited. + +The protocol keeps track of how much was staked by the user in `voting` and `build&earn` subperiod. This is important for the bonus reward calculation. + +It is not possible to stake on a dApp that has been unregistered. +However, if dApp is unregistered after user has staked on it, user will keep earning +rewards for the staked amount. + +#### Unstaking Tokens + +User can at any time decide to unstake staked tokens. There's no _unstaking_ process associated with this action. + +Unlike stake operation, which stakes from the _next_ era, unstake will reduce the staked amount for the current and next era if stake exists. + +Same as with stake operation, it's not possible to unstake anything until unclaimed rewards have been claimed. User must ensure to first claim all rewards, before attempting to unstake. Unstake amount must also be precise as no adjustment will be done to the amount. + +The amount unstaked will always first reduce the amount staked in the ongoing subperiod. E.g. if `voting` subperiod has stake of **100**, and `build&earn` subperiod has stake of **50**, calling unstake with amount **70** during `build&earn` subperiod will see `build&earn` stake amount reduced to **zero**, while `voting` stake will be reduced to **80**. + +If unstake would reduce the staked amount below `MinimumStakeAmount`, everything is unstaked. + +Once period finishes, all stakes are reset back to zero. This means that no unstake operation is needed after period ends to _unstake_ funds - it's done automatically. + +If dApp has been unregistered, a special operation to unstake from unregistered contract must be used. + +#### Claiming Staker Rewards + +Stakers can claim rewards for passed eras during which they were staking. Even if multiple contracts were staked, claim reward call will claim rewards for all of them. + +Only rewards for passed eras can be claimed. It is possible that a successful reward claim call will claim rewards for multiple eras. This can happen if staker hasn't claimed rewards in some time, and many eras have passed since then, accumulating pending rewards. + +To achieve this, the pallet's underyling storage organizes **era reward information** into **spans**. A single span covers multiple eras, e.g. from **1** to **16**. In case user has staked during era 1, and hasn't claimed rewards until era 17, they will be eligible to claim 15 rewards in total (from era 2 to 16). All of this will be done in a single claim reward call. + +In case unclaimed history has built up past one span, multiple reward claim calls will be needed to claim all of the rewards. + +Rewards don't remain available forever, and if not claimed within some time period, they will be treated as expired. This will be a longer period, but will still exist. + +Rewards are calculated using a simple formula: `staker_reward_pool * staker_staked_amount / total_staked_amount`. + +#### Claim Bonus Reward + +If staker staked on a dApp during the voting period, and didn't reduce their staked amount below what was staked at the end of the voting period, this makes them eligible for the bonus reward. + +Bonus rewards need to be claimed per contract, unlike staker rewards. + +Bonus reward is calculated using a simple formula: `bonus_reward_pool * staker_voting_period_stake / total_voting_period_stake`. + From fd65f20004f3162cedf1e916c7d770f81686a804 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 7 Nov 2023 14:43:54 +0100 Subject: [PATCH 49/86] Minor addition --- pallets/dapp-staking-v3/src/lib.rs | 2 +- pallets/dapp-staking-v3/src/test/tests.rs | 24 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index cd63dff29e..5d224ef83c 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1617,7 +1617,6 @@ pub mod pallet { // // Experiment with an 'experimental' entry shows PoV size of ~7kB induced for entry that can hold up to 100 entries. - let tier_config = TierConfig::::get(); let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); // 1. @@ -1653,6 +1652,7 @@ pub mod pallet { // 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 = Vec::with_capacity(dapp_stakes.len()); + let tier_config = TierConfig::::get(); let mut global_idx = 0; let mut tier_id = 0; diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 90de8bbb7c..dda2dd7e4c 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -35,7 +35,29 @@ fn print_test() { use crate::dsv3_weight::WeightInfo; println!( ">>> dApp tier assignment reading & calculation {:?}", - crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(100) + crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(200) + ); + + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + + #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] + struct RewardSize; + impl Get for RewardSize { + fn get() -> u32 { + 1_00_u32 + } + } + #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] + struct TierSize; + impl Get for TierSize { + fn get() -> u32 { + 4_u32 + } + } + println!( + ">>> Max encoded size for dapp tier rewards: {:?}", + crate::DAppTierRewards::::max_encoded_len() ); println!( From 8fe1563b3cb58376fb850d68e2d0668a17d329aa Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 9 Nov 2023 17:10:17 +0100 Subject: [PATCH 50/86] Review comment fixes & changes --- pallets/dapp-staking-v3/src/lib.rs | 11 +++++--- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- pallets/dapp-staking-v3/src/test/tests.rs | 4 +-- .../dapp-staking-v3/src/test/tests_types.rs | 4 +-- pallets/dapp-staking-v3/src/types.rs | 27 ++++++++----------- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 5d224ef83c..2687f0ec7a 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -529,7 +529,7 @@ pub mod pallet { let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); protocol_state - .into_next_subperiod(subperiod_end_era, build_and_earn_start_block); + .advance_to_next_subperiod(subperiod_end_era, build_and_earn_start_block); era_info.migrate_to_next_era(Some(protocol_state.subperiod())); @@ -581,7 +581,8 @@ pub mod pallet { let voting_period_length = Self::blocks_per_voting_period(); let next_era_start_block = now.saturating_add(voting_period_length); - protocol_state.into_next_subperiod(subperiod_end_era, next_era_start_block); + protocol_state + .advance_to_next_subperiod(subperiod_end_era, next_era_start_block); era_info.migrate_to_next_era(Some(protocol_state.subperiod())); @@ -775,7 +776,7 @@ pub mod pallet { /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. /// - /// Can be called by dApp owner or dApp staking manager origin. + /// Can be called by dApp staking manager origin. #[pallet::call_index(4)] #[pallet::weight(Weight::zero())] pub fn unregister( @@ -1503,7 +1504,9 @@ pub mod pallet { Ok(()) } - /// Used to enable or disable maintenance mode. + /// Used to force a change of era or subperiod. + /// The effect isn't immediate but will happen on the next block. + /// /// Can only be called by manager origin. #[pallet::call_index(16)] #[pallet::weight(Weight::zero())] diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index f186f7e5a6..6b1a16287f 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -304,7 +304,7 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_into_next_subperiod() { +pub(crate) fn advance_to_advance_to_next_subperiod() { let subperiod = ActiveProtocolState::::get().subperiod(); while ActiveProtocolState::::get().subperiod() == subperiod { run_for_blocks(1); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index dda2dd7e4c..996a56f1e9 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1314,7 +1314,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_period( ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); - advance_to_into_next_subperiod(); + advance_to_advance_to_next_subperiod(); advance_to_era( ActiveProtocolState::::get() .period_info @@ -1444,7 +1444,7 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { assert_lock(account, lock_amount); // Stake in Build&Earn period type, advance to next era and try to claim bonus reward - advance_to_into_next_subperiod(); + advance_to_advance_to_next_subperiod(); assert_eq!( ActiveProtocolState::::get().subperiod(), Subperiod::BuildAndEarn, diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 71f13e2bdd..6554faed11 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -118,7 +118,7 @@ fn protocol_state_basic_checks() { // Toggle new period type check - 'Voting' to 'BuildAndEarn' let subperiod_end_era_1 = 23; let next_era_start_1 = 41; - protocol_state.into_next_subperiod(subperiod_end_era_1, next_era_start_1); + protocol_state.advance_to_next_subperiod(subperiod_end_era_1, next_era_start_1); assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!( protocol_state.period_number(), @@ -132,7 +132,7 @@ fn protocol_state_basic_checks() { // Toggle from 'BuildAndEarn' over to 'Voting' let subperiod_end_era_2 = 24; let next_era_start_2 = 91; - protocol_state.into_next_subperiod(subperiod_end_era_2, next_era_start_2); + protocol_state.advance_to_next_subperiod(subperiod_end_era_2, next_era_start_2); assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!( protocol_state.period_number(), diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 8f48239666..9848b3f933 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -25,7 +25,7 @@ //! # Overview //! //! The following is a high level overview of the implemented structs, enums & types. -//! For detailes, please refer to the documentation and code of each individual type. +//! For details, please refer to the documentation and code of each individual type. //! //! ## General Protocol Information //! @@ -47,7 +47,7 @@ //! * `UnlockingChunk` - describes some amount undergoing the unlocking process. //! * `StakeAmount` - contains information about the staked amount in a particular era, and period. //! * `AccountLedger` - keeps track of total locked & staked balance, unlocking chunks and number of stake entries. -//! * `SingularStakingInfo` - contains information about a particular stakers stake on a specific smart contract. Used to track loyalty. +//! * `SingularStakingInfo` - contains information about a particular staker's stake on a specific smart contract. Used to track loyalty. //! //! ## Era Information //! @@ -61,7 +61,7 @@ //! * `TierParameters` - contains static information about tiers, like init thresholds, reward & slot distribution. //! * `TiersConfiguration` - contains dynamic information about tiers, derived from `TierParameters` and onchain data. //! * `DAppTier` - a compact struct describing a dApp's tier. -//! * `DAppTierREwards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. +//! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. //! //! TODO: some types are missing so double check before final merge that everything is covered and explained correctly @@ -150,7 +150,7 @@ pub struct PeriodInfo { pub number: PeriodNumber, /// subperiod. pub subperiod: Subperiod, - /// Last ear of the subperiod, after this a new subperiod should start. + /// Last era of the subperiod, after this a new subperiod should start. #[codec(compact)] pub subperiod_end_era: EraNumber, } @@ -245,7 +245,7 @@ where } /// Triggers the next subperiod, updating appropriate parameters. - pub fn into_next_subperiod( + pub fn advance_to_next_subperiod( &mut self, subperiod_end_era: EraNumber, next_era_start: BlockNumber, @@ -535,7 +535,7 @@ where // In case it doesn't (i.e. first time staking), then the future era must match exactly // one era after the one provided via argument. } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era + 1 { + if stake_amount.era != era.saturating_add(1) { return Err(AccountLedgerError::InvalidEra); } if stake_amount.period != current_period_info.number { @@ -552,11 +552,11 @@ where /// 1. The `period` requirement enforces staking in the ongoing period. /// 2. The `era` requirement enforces staking in the ongoing era. /// - /// The 2nd condition is needed to prevent stakers from building a significant histort of stakes, + /// The 2nd condition is needed to prevent stakers from building a significant history of stakes, /// without claiming the rewards. So if a historic era exists as an entry, stakers will first need to claim /// the pending rewards, before they can stake again. /// - /// Additonally, the staked amount must not exceed what's available for staking. + /// Additionally, the staked amount must not exceed what's available for staking. pub fn add_stake_amount( &mut self, amount: Balance, @@ -580,7 +580,7 @@ where } None => { let mut stake_amount = self.staked; - stake_amount.era = era + 1; + stake_amount.era = era.saturating_add(1); stake_amount.period = current_period_info.number; stake_amount.add(amount, current_period_info.subperiod); self.staked_future = Some(stake_amount); @@ -1614,15 +1614,10 @@ impl, NT: Get> DAppTierRewards { /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`. pub fn try_consume(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { // Check if dApp Id exists. - let dapp_idx = match self + let dapp_idx = self .dapps .binary_search_by(|entry| entry.dapp_id.cmp(&dapp_id)) - { - Ok(idx) => idx, - _ => { - return Err(DAppTierError::NoDAppInTiers); - } - }; + .map_err(|_| DAppTierError::NoDAppInTiers)?; match self.dapps.get_mut(dapp_idx) { Some(dapp_tier) => { From 3b894ef600b3070ac89d3c5df62494651a4cf013 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 10 Nov 2023 10:29:11 +0100 Subject: [PATCH 51/86] Minor change --- pallets/dapp-staking-v3/src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 9848b3f933..8949519e5a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -988,6 +988,7 @@ impl SingularStakingInfo { /// Stake the specified amount on the contract, for the specified subperiod. pub fn stake(&mut self, amount: Balance, subperiod: Subperiod) { + // TODO: if we keep `StakeAmount` type here, consider including the era as well for consistency self.staked.add(amount, subperiod); } From 7cc82da28b23e9c84729ba4f7bf4464e60b22f20 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 10 Nov 2023 17:00:01 +0100 Subject: [PATCH 52/86] Review comments --- pallets/dapp-staking-v3/README.md | 2 +- pallets/dapp-staking-v3/src/lib.rs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index 68cd444b1c..afae398db2 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -2,7 +2,7 @@ ## Introduction -Astar and Shiden networks provide a unique way for developers to earn rewards by developing products that native token holdes decide to support. +Astar and Shiden networks provide a unique way for developers to earn rewards by developing products that native token holders decide to support. The principle is simple - stakers lock their tokens to _stake_ on a dApp, and if the dApp attracts enough support, it is rewarded in native currency, derived from the inflation. In turn stakers are rewarded for locking & staking their tokens. diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 2687f0ec7a..1a85e00348 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -454,10 +454,9 @@ pub mod pallet { ); // Prepare tier configuration and verify its correctness - let number_of_slots = self - .slots_per_tier - .iter() - .fold(0, |acc, &slots| acc + slots); + let number_of_slots = self.slots_per_tier.iter().fold(0_u16, |acc, &slots| { + acc.checked_add(slots).expect("Overflow") + }); let tier_config = TiersConfiguration:: { number_of_slots, slots_per_tier: BoundedVec::::try_from( @@ -1269,6 +1268,7 @@ pub mod pallet { reward_sum.saturating_accrue(staker_reward); } + // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. // Account exists since it has locked funds. T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; @@ -1332,6 +1332,7 @@ pub mod pallet { Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; + // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. // Account exists since it has locked funds. T::Currency::deposit_into_existing(&account, bonus_reward) .map_err(|_| Error::::InternalClaimStakerError)?; @@ -1358,6 +1359,8 @@ pub mod pallet { #[pallet::compact] era: EraNumber, ) -> DispatchResult { Self::ensure_pallet_enabled()?; + + // TODO: Shall we make sure only dApp owner or beneficiary can trigger the claim? let _ = ensure_signed(origin)?; let dapp_info = @@ -1385,6 +1388,7 @@ pub mod pallet { // Get reward destination, and deposit the reward. let beneficiary = dapp_info.reward_beneficiary(); + // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. T::Currency::deposit_creating(beneficiary, amount); // Write back updated struct to prevent double reward claims @@ -1507,6 +1511,9 @@ pub mod pallet { /// Used to force a change of era or subperiod. /// The effect isn't immediate but will happen on the next block. /// + /// Used for testing purposes, when we want to force an era change, or a subperiod change. + /// Not intended to be used in production, except in case of unforseen circumstances. + /// /// Can only be called by manager origin. #[pallet::call_index(16)] #[pallet::weight(Weight::zero())] From cc1efcc998fc2ac7dc2e15c367fa9cb15dbd1ef8 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 13 Nov 2023 08:28:49 +0100 Subject: [PATCH 53/86] Update frontier to make CI pass --- Cargo.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf8bfcc37a..fe18d417f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3066,7 +3066,7 @@ dependencies = [ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "async-trait", "fp-consensus", @@ -3082,7 +3082,7 @@ dependencies = [ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "async-trait", "fp-storage", @@ -3102,7 +3102,7 @@ dependencies = [ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fc-db", "fc-storage", @@ -3123,7 +3123,7 @@ dependencies = [ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3173,7 +3173,7 @@ dependencies = [ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3186,7 +3186,7 @@ dependencies = [ [[package]] name = "fc-storage" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3338,7 +3338,7 @@ dependencies = [ [[package]] name = "fp-account" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "hex", "impl-serde", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "parity-scale-codec", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "fp-ethereum" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3383,7 +3383,7 @@ dependencies = [ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "evm", "frame-support", @@ -3398,7 +3398,7 @@ dependencies = [ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3415,7 +3415,7 @@ dependencies = [ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "frame-support", "parity-scale-codec", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "parity-scale-codec", "serde", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "frame-support", @@ -7226,7 +7226,7 @@ dependencies = [ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -7273,7 +7273,7 @@ dependencies = [ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "environmental", "evm", @@ -7298,7 +7298,7 @@ dependencies = [ [[package]] name = "pallet-evm-chain-id" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "frame-support", "frame-system", @@ -7363,7 +7363,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", ] @@ -7371,7 +7371,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-bn128" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "sp-core", @@ -7406,7 +7406,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-dispatch" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "frame-support", @@ -7416,7 +7416,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-ed25519" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ed25519-dalek", "fp-evm", @@ -7425,7 +7425,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "num", @@ -7434,7 +7434,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-sha3fips" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "tiny-keccak", @@ -7443,7 +7443,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-simple" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "ripemd", From ab77e36021ca3e7dd6126bde13e88307110e2202 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 13 Nov 2023 08:59:37 +0100 Subject: [PATCH 54/86] dApp staking v3 - part 5 --- pallets/dapp-staking-v3/src/benchmarking.rs | 11 --- pallets/dapp-staking-v3/src/lib.rs | 80 ++++++------------- .../dapp-staking-v3/src/test/testing_utils.rs | 14 ++-- pallets/dapp-staking-v3/src/test/tests.rs | 5 -- .../dapp-staking-v3/src/test/tests_types.rs | 2 + pallets/dapp-staking-v3/src/types.rs | 52 ++++-------- 6 files changed, 51 insertions(+), 113 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index e98fd1ce09..b9c591e264 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -217,17 +217,6 @@ mod benchmarks { } } - #[benchmark] - fn experimental_read() { - // Prepare init config (protocol state, tier params & config, etc.) - initial_config::(); - - #[block] - { - let _ = ExperimentalContractEntries::::get(10); - } - } - impl_benchmark_test_suite!( Pallet, crate::benchmarking::tests::new_test_ext(), diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 1a85e00348..8ea473196a 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -372,7 +372,7 @@ pub mod pallet { /// Information about how much has been staked on a smart contract in some era or period. #[pallet::storage] pub type ContractStake = - StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakeAmount, ValueQuery>; + StorageMap<_, Twox64Concat, DAppId, ContractStakeAmount, ValueQuery>; /// General information about the current era. #[pallet::storage] @@ -416,11 +416,6 @@ pub mod pallet { pub type DAppTiers = StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; - // TODO: this is experimental, please don't review - #[pallet::storage] - pub type ExperimentalContractEntries = - StorageMap<_, Twox64Concat, EraNumber, ContractEntriesFor, OptionQuery>; - #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -679,6 +674,7 @@ pub mod pallet { id: dapp_id, state: DAppState::Registered, reward_destination: None, + tier_label: None, }, ); @@ -787,25 +783,18 @@ pub mod pallet { let current_era = ActiveProtocolState::::get().era; - IntegratedDApps::::try_mutate( - &smart_contract, - |maybe_dapp_info| -> DispatchResult { - let dapp_info = maybe_dapp_info - .as_mut() - .ok_or(Error::::ContractNotFound)?; + let mut dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::ContractNotFound)?; - ensure!( - dapp_info.state == DAppState::Registered, - Error::::NotOperatedDApp - ); + ensure!( + dapp_info.state == DAppState::Registered, + Error::::NotOperatedDApp + ); - dapp_info.state = DAppState::Unregistered(current_era); + ContractStake::::remove(&dapp_info.id); - Ok(()) - }, - )?; - - ContractStake::::remove(&smart_contract); + dapp_info.state = DAppState::Unregistered(current_era); + IntegratedDApps::::insert(&smart_contract, dapp_info); Self::deposit_event(Event::::DAppUnregistered { smart_contract, @@ -990,10 +979,9 @@ pub mod pallet { ensure!(amount > 0, Error::::ZeroAmount); - ensure!( - Self::is_active(&smart_contract), - Error::::NotOperatedDApp - ); + let dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; + ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); let stake_era = protocol_state.era; @@ -1069,7 +1057,7 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the new stake amount on the specified contract. - let mut contract_stake_info = ContractStake::::get(&smart_contract); + let mut contract_stake_info = ContractStake::::get(&dapp_info.id); contract_stake_info.stake(amount, protocol_state.period_info, stake_era); // 4. @@ -1082,7 +1070,7 @@ pub mod pallet { // Update remaining storage entries Self::update_ledger(&account, ledger); StakerInfo::::insert(&account, &smart_contract, new_staking_info); - ContractStake::::insert(&smart_contract, contract_stake_info); + ContractStake::::insert(&dapp_info.id, contract_stake_info); Self::deposit_event(Event::::Stake { account, @@ -1109,10 +1097,9 @@ pub mod pallet { ensure!(amount > 0, Error::::ZeroAmount); - ensure!( - Self::is_active(&smart_contract), - Error::::NotOperatedDApp - ); + let dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; + ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); let unstake_era = protocol_state.era; @@ -1167,7 +1154,7 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the reduced stake amount on the specified contract. - let mut contract_stake_info = ContractStake::::get(&smart_contract); + let mut contract_stake_info = ContractStake::::get(&dapp_info.id); contract_stake_info.unstake(amount, protocol_state.period_info, unstake_era); // 4. @@ -1178,7 +1165,7 @@ pub mod pallet { // 5. // Update remaining storage entries - ContractStake::::insert(&smart_contract, contract_stake_info); + ContractStake::::insert(&dapp_info.id, contract_stake_info); if new_staking_info.is_empty() { ledger.contract_stake_count.saturating_dec(); @@ -1615,31 +1602,14 @@ pub mod pallet { period: PeriodNumber, dapp_reward_pool: Balance, ) -> DAppTierRewardsFor { - // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. - // Benchmarks will show this, but I don't believe it will be needed, especially with increased block capacity we'll get with async backing. - // Even without async backing though, we should have enough capacity to handle this. - // UPDATE: might work with async backing, but right now we could handle up to 150 dApps before exceeding the PoV size. - - // UPDATE2: instead of taking the approach of reading an ever increasing amount of entries from storage, we can instead adopt an approach - // of eficiently storing composite information into `BTreeMap`. The approach is essentially the same as the one used below to store rewards. - // Each time `stake` or `unstake` are called, corresponding entries are updated. This way we can keep track of all the contract stake in a single DB entry. - // To make the solution more scalable, we could 'split' stake entries into spans, similar as rewards are handled now. - // - // Experiment with an 'experimental' entry shows PoV size of ~7kB induced for entry that can hold up to 100 entries. - let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); // 1. - // Iterate over all registered dApps, and collect their stake amount. + // Iterate over all staked dApps. // This is bounded by max amount of dApps we allow to be registered. - for (smart_contract, dapp_info) in IntegratedDApps::::iter() { - // Skip unregistered dApps - if dapp_info.state != DAppState::Registered { - continue; - } - + for (dapp_id, stake_amount) in ContractStake::::iter() { // Skip dApps which don't have ANY amount staked (TODO: potential improvement is to prune all dApps below minimum threshold) - let stake_amount = match ContractStake::::get(&smart_contract).get(era, period) { + let stake_amount = match stake_amount.get(era, period) { Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, _ => continue, }; @@ -1650,7 +1620,7 @@ pub mod pallet { // 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())); + dapp_stakes.push((dapp_id, stake_amount.total())); } // 2. diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 3ac63253ab..0d66143d84 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -47,7 +47,7 @@ pub(crate) struct MemorySnapshot { ), SingularStakingInfo, >, - contract_stake: HashMap<::SmartContract, ContractStakeAmount>, + contract_stake: HashMap, era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, dapp_tiers: HashMap>, @@ -187,7 +187,9 @@ pub(crate) fn assert_unregister(smart_contract: &MockSmartContract) { IntegratedDApps::::get(&smart_contract).unwrap().state, DAppState::Unregistered(pre_snapshot.active_protocol_state.era), ); - assert!(!ContractStake::::contains_key(&smart_contract)); + assert!(!ContractStake::::contains_key( + &IntegratedDApps::::get(&smart_contract).unwrap().id + )); } /// Lock funds into dApp staking and assert success. @@ -431,7 +433,7 @@ pub(crate) fn assert_stake( .get(&(account, smart_contract.clone())); let pre_contract_stake = pre_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .map_or(ContractStakeAmount::default(), |series| series.clone()); let pre_era_info = pre_snapshot.current_era_info; @@ -460,7 +462,7 @@ pub(crate) fn assert_stake( .expect("Entry must exist since 'stake' operation was successfull."); let post_contract_stake = post_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .expect("Entry must exist since 'stake' operation was successfull."); let post_era_info = post_snapshot.current_era_info; @@ -580,7 +582,7 @@ pub(crate) fn assert_unstake( .expect("Entry must exist since 'unstake' is being called."); let pre_contract_stake = pre_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .expect("Entry must exist since 'unstake' is being called."); let pre_era_info = pre_snapshot.current_era_info; @@ -616,7 +618,7 @@ pub(crate) fn assert_unstake( let post_ledger = post_snapshot.ledger.get(&account).unwrap(); let post_contract_stake = post_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .expect("Entry must exist since 'stake' operation was successfull."); let post_era_info = post_snapshot.current_era_info; diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 996a56f1e9..f652072241 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -59,11 +59,6 @@ fn print_test() { ">>> Max encoded size for dapp tier rewards: {:?}", crate::DAppTierRewards::::max_encoded_len() ); - - println!( - ">>> Experimental storage entry read {:?}", - crate::dsv3_weight::SubstrateWeight::::experimental_read() - ); }) } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 6554faed11..34de7c96e0 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -153,6 +153,7 @@ fn dapp_info_basic_checks() { id: 7, state: DAppState::Registered, reward_destination: None, + tier_label: None, }; // Owner receives reward in case no beneficiary is set @@ -1374,6 +1375,7 @@ fn contract_stake_info_get_works() { let contract_stake = ContractStakeAmount { staked: info_1, staked_future: Some(info_2), + tier_label: None, }; // Sanity check diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 8949519e5a..a4384f119c 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -286,6 +286,8 @@ pub struct DAppInfo { pub state: DAppState, // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`. pub reward_destination: Option, + /// If `Some(_)` dApp has a tier label which can influence the tier assignment. + pub tier_label: Option, } impl DAppInfo { @@ -296,6 +298,11 @@ impl DAppInfo { None => &self.owner, } } + + /// `true` if dApp is still active (registered), `false` otherwise. + pub fn is_active(&self) -> bool { + self.state == DAppState::Registered + } } /// How much was unlocked in some block. @@ -1056,7 +1063,10 @@ pub struct ContractStakeAmount { pub staked: StakeAmount, /// Staked amount in the next or 'future' era. pub staked_future: Option, + /// Tier label for the contract, if any. + pub tier_label: Option, } + impl ContractStakeAmount { /// `true` if series is empty, `false` otherwise. pub fn is_empty(&self) -> bool { @@ -1649,6 +1659,12 @@ pub enum DAppTierError { InternalError, } +/// Tier labels can be assigned to dApps in order to provide them benefits (or drawbacks) when being assigned into a tier. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub enum TierLabel { + // Empty for now, on purpose. +} + /////////////////////////////////////////////////////////////////////// //////////// MOVE THIS TO SOME PRIMITIVES CRATE LATER //////////// /////////////////////////////////////////////////////////////////////// @@ -1682,39 +1698,3 @@ pub trait RewardPoolProvider { /// Get the bonus pool for stakers. fn bonus_reward_pool() -> Balance; } - -// TODO: these are experimental, don't review -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] -pub struct ExperimentalContractStakeEntry { - #[codec(compact)] - pub dapp_id: DAppId, - #[codec(compact)] - pub voting: Balance, - #[codec(compact)] - pub build_and_earn: Balance, -} - -#[derive( - Encode, - Decode, - MaxEncodedLen, - RuntimeDebugNoBound, - PartialEqNoBound, - EqNoBound, - CloneNoBound, - TypeInfo, -)] -#[scale_info(skip_type_params(MD, NT))] -pub struct ExperimentalContractStakeEntries, 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, - /// Period during which this struct was created. - #[codec(compact)] - pub period: PeriodNumber, -} - -// TODO: temp experimental type, don't review -pub type ContractEntriesFor = - ExperimentalContractStakeEntries, ::NumberOfTiers>; From f05e19b25902b8ec5d45d9732188f50e90bfc5a5 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 14 Nov 2023 08:08:44 +0100 Subject: [PATCH 55/86] Make check work --- Cargo.lock | 48 ++++++------- pallets/dapp-staking-v3/src/benchmarking.rs | 1 + pallets/dapp-staking-v3/src/dsv3_weight.rs | 75 +++++++-------------- pallets/dapp-staking-v3/src/lib.rs | 6 -- pallets/dapp-staking-v3/src/test/tests.rs | 4 +- 5 files changed, 53 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe18d417f9..c235a3ebf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3066,7 +3066,7 @@ dependencies = [ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "async-trait", "fp-consensus", @@ -3082,7 +3082,7 @@ dependencies = [ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "async-trait", "fp-storage", @@ -3102,7 +3102,7 @@ dependencies = [ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fc-db", "fc-storage", @@ -3123,7 +3123,7 @@ dependencies = [ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3173,7 +3173,7 @@ dependencies = [ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3186,7 +3186,7 @@ dependencies = [ [[package]] name = "fc-storage" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3338,7 +3338,7 @@ dependencies = [ [[package]] name = "fp-account" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "hex", "impl-serde", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "parity-scale-codec", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "fp-ethereum" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3383,7 +3383,7 @@ dependencies = [ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "evm", "frame-support", @@ -3398,7 +3398,7 @@ dependencies = [ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3415,7 +3415,7 @@ dependencies = [ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "frame-support", "parity-scale-codec", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "parity-scale-codec", "serde", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "frame-support", @@ -7226,7 +7226,7 @@ dependencies = [ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -7273,7 +7273,7 @@ dependencies = [ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "environmental", "evm", @@ -7298,7 +7298,7 @@ dependencies = [ [[package]] name = "pallet-evm-chain-id" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "frame-support", "frame-system", @@ -7363,7 +7363,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", ] @@ -7371,7 +7371,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-bn128" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "sp-core", @@ -7406,7 +7406,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-dispatch" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "frame-support", @@ -7416,7 +7416,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-ed25519" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ed25519-dalek", "fp-evm", @@ -7425,7 +7425,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "num", @@ -7434,7 +7434,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-sha3fips" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "tiny-keccak", @@ -7443,7 +7443,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-simple" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "ripemd", diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index b9c591e264..243786a0af 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -165,6 +165,7 @@ fn max_number_of_contracts() -> u32 { mod benchmarks { use super::*; + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. #[benchmark] fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { // Prepare init config (protocol state, tier params & config, etc.) diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs index 1dca8a56ce..8568c3a152 100644 --- a/pallets/dapp-staking-v3/src/dsv3_weight.rs +++ b/pallets/dapp-staking-v3/src/dsv3_weight.rs @@ -20,7 +20,7 @@ //! Autogenerated weights for pallet_dapp_staking_v3 //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-11-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `Dinos-MBP`, CPU: `` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -32,12 +32,12 @@ // --chain=dev // --steps=50 // --repeat=20 -// --pallet=pallet_dapp_staking_v3 +// --pallet=pallet_dapp_staking-v3 // --extrinsic=* // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=dsv3_weight.rs +// --output=dapp_staking_v3.rs // --template=./scripts/templates/weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -50,76 +50,51 @@ use core::marker::PhantomData; /// Weight functions needed for pallet_dapp_staking_v3. pub trait WeightInfo { fn dapp_tier_assignment(x: u32, ) -> Weight; - fn experimental_read() -> Weight; } /// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking IntegratedDApps (r:101 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:100 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(93), added: 2568, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `836 + x * (169 ±0)` - // Estimated: `3586 + x * (2605 ±0)` + // Measured: `449 + x * (33 ±0)` + // Estimated: `3558 + x * (2568 ±0)` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(12_879_631, 3586) - // Standard Error: 18_480 - .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) + Weight::from_parts(15_553_205, 3558) + // Standard Error: 9_476 + .saturating_add(Weight::from_parts(2_714_919, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) - } - /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) - /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) - fn experimental_read() -> Weight { - // Proof Size summary in bytes: - // Measured: `224` - // Estimated: `6948` - // Minimum execution time: 5_000_000 picoseconds. - Weight::from_parts(5_000_000, 6948) - .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2568).saturating_mul(x.into())) } } // For backwards compatibility and tests impl WeightInfo for () { - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking IntegratedDApps (r:101 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:100 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(93), added: 2568, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `836 + x * (169 ±0)` - // Estimated: `3586 + x * (2605 ±0)` + // Measured: `449 + x * (33 ±0)` + // Estimated: `3558 + x * (2568 ±0)` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(12_879_631, 3586) - // Standard Error: 18_480 - .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) + Weight::from_parts(15_553_205, 3558) + // Standard Error: 9_476 + .saturating_add(Weight::from_parts(2_714_919, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) - .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) - } - /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) - /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) - fn experimental_read() -> Weight { - // Proof Size summary in bytes: - // Measured: `224` - // Estimated: `6948` - // Minimum execution time: 5_000_000 picoseconds. - Weight::from_parts(5_000_000, 6948) - .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2568).saturating_mul(x.into())) } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8ea473196a..6c6494fee7 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1614,12 +1614,6 @@ pub mod pallet { _ => continue, }; - // TODO: Need to handle labels! - // 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_id, stake_amount.total())); } diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index f652072241..4b0bbd88a3 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -35,7 +35,7 @@ fn print_test() { use crate::dsv3_weight::WeightInfo; println!( ">>> dApp tier assignment reading & calculation {:?}", - crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(200) + crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(100) ); use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -59,6 +59,8 @@ fn print_test() { ">>> Max encoded size for dapp tier rewards: {:?}", crate::DAppTierRewards::::max_encoded_len() ); + + println!(">>> Max encoded size of ContractStake: {:?}", crate::ContractStakeAmount::max_encoded_len()); }) } From 6269499cff76561db20dc5f4f09b20e7ccfd60be Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 14 Nov 2023 08:44:32 +0100 Subject: [PATCH 56/86] Limit map size to improve benchmarks --- pallets/dapp-staking-v3/src/benchmarking.rs | 5 ++-- pallets/dapp-staking-v3/src/dsv3_weight.rs | 26 ++++++++++----------- pallets/dapp-staking-v3/src/lib.rs | 24 ++++++++++++------- pallets/dapp-staking-v3/src/test/mock.rs | 4 ++-- pallets/dapp-staking-v3/src/test/tests.rs | 5 +++- pallets/dapp-staking-v3/src/types.rs | 11 +-------- runtime/local/src/lib.rs | 4 ++-- 7 files changed, 40 insertions(+), 39 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 243786a0af..a3a561a1bc 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -87,7 +87,7 @@ const UNIT: Balance = 1_000_000_000_000_000_000; // Minimum amount that must be staked on a dApp to enter any tier const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; -const NUMBER_OF_SLOTS: u16 = 100; +const NUMBER_OF_SLOTS: u32 = 100; pub fn initial_config() { let era_length = T::StandardEraLength::get(); @@ -143,7 +143,7 @@ pub fn initial_config() { // Init tier config, based on the initial params let init_tier_config = TiersConfiguration:: { - number_of_slots: NUMBER_OF_SLOTS, + number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), reward_portion: tier_params.reward_portion.clone(), tier_thresholds: tier_params.tier_thresholds.clone(), @@ -166,6 +166,7 @@ mod benchmarks { use super::*; // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. + // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs #[benchmark] fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { // Prepare init config (protocol state, tier params & config, etc.) diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs index 8568c3a152..c1f88e9f1a 100644 --- a/pallets/dapp-staking-v3/src/dsv3_weight.rs +++ b/pallets/dapp-staking-v3/src/dsv3_weight.rs @@ -20,7 +20,7 @@ //! Autogenerated weights for pallet_dapp_staking_v3 //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-11-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `Dinos-MBP`, CPU: `` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -58,21 +58,21 @@ impl WeightInfo for SubstrateWeight { /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(93), added: 2568, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) /// Storage: DappStaking TierConfig (r:1 w:0) /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `449 + x * (33 ±0)` - // Estimated: `3558 + x * (2568 ±0)` + // Estimated: `3063 + x * (2073 ±0)` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(15_553_205, 3558) - // Standard Error: 9_476 - .saturating_add(Weight::from_parts(2_714_919, 0).saturating_mul(x.into())) + Weight::from_parts(16_776_512, 3063) + // Standard Error: 3_400 + .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2568).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) } } @@ -81,20 +81,20 @@ impl WeightInfo for () { /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(93), added: 2568, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) /// Storage: DappStaking TierConfig (r:1 w:0) /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `449 + x * (33 ±0)` - // Estimated: `3558 + x * (2568 ±0)` + // Estimated: `3063 + x * (2073 ±0)` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(15_553_205, 3558) - // Standard Error: 9_476 - .saturating_add(Weight::from_parts(2_714_919, 0).saturating_mul(x.into())) + Weight::from_parts(16_776_512, 3063) + // Standard Error: 3_400 + .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2568).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 6c6494fee7..22e0860a39 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -68,6 +68,7 @@ pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; mod dsv3_weight; +// Lock identifier for the dApp staking pallet const STAKING_ID: LockIdentifier = *b"dapstake"; // TODO: add tracing! @@ -139,7 +140,7 @@ pub mod pallet { /// Maximum number of contracts that can be integrated into dApp staking at once. #[pallet::constant] - type MaxNumberOfContracts: Get; + type MaxNumberOfContracts: Get; /// Maximum number of unlocking chunks that can exist per account at a time. #[pallet::constant] @@ -345,11 +346,11 @@ pub mod pallet { /// Map of all dApps integrated into dApp staking protocol. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< - _, - Blake2_128Concat, - T::SmartContract, - DAppInfo, - OptionQuery, + Hasher = Blake2_128Concat, + Key = T::SmartContract, + Value = DAppInfo, + QueryKind = OptionQuery, + MaxValues = ConstU32<{ DAppId::MAX as u32 }>, >; /// General locked/staked information for each account. @@ -371,8 +372,13 @@ pub mod pallet { /// Information about how much has been staked on a smart contract in some era or period. #[pallet::storage] - pub type ContractStake = - StorageMap<_, Twox64Concat, DAppId, ContractStakeAmount, ValueQuery>; + pub type ContractStake = StorageMap< + Hasher = Twox64Concat, + Key = DAppId, + Value = ContractStakeAmount, + QueryKind = ValueQuery, + MaxValues = ConstU32<{ DAppId::MAX as u32 }>, + >; /// General information about the current era. #[pallet::storage] @@ -1674,7 +1680,7 @@ pub mod pallet { // 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( + DAppTierRewards::::new( dapp_tiers, tier_rewards, period, diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 6b1a16287f..c97a563f1f 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -20,7 +20,7 @@ use crate::{self as pallet_dapp_staking, *}; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU16, ConstU32, ConstU64}, + traits::{ConstU128, ConstU32, ConstU64}, weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -159,7 +159,7 @@ impl pallet_dapp_staking::Config for Test { type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<10>; + type MaxNumberOfContracts = ConstU32<10>; type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU32<2>; diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 4b0bbd88a3..9fa4844083 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -60,7 +60,10 @@ fn print_test() { crate::DAppTierRewards::::max_encoded_len() ); - println!(">>> Max encoded size of ContractStake: {:?}", crate::ContractStakeAmount::max_encoded_len()); + println!( + ">>> Max encoded size of ContractStake: {:?}", + crate::ContractStakeAmount::max_encoded_len() + ); }) } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index a4384f119c..63b80c9867 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -84,16 +84,7 @@ pub type AccountLedgerFor = AccountLedger, ::M // Convenience type for `DAppTierRewards` usage. pub type DAppTierRewardsFor = - DAppTierRewards, ::NumberOfTiers>; - -// Helper struct for converting `u16` getter into `u32` -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct MaxNumberOfContractsU32(PhantomData); -impl Get for MaxNumberOfContractsU32 { - fn get() -> u32 { - T::MaxNumberOfContracts::get() as u32 - } -} + DAppTierRewards<::MaxNumberOfContracts, ::NumberOfTiers>; /// Era number type pub type EraNumber = u32; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 12ec1e4062..d3ee064fb1 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -27,7 +27,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, ConstU64, Currency, EitherOfDiverse, + AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, }, weights::{ @@ -483,7 +483,7 @@ impl pallet_dapp_staking_v3::Config for Runtime { type StandardErasPerBuildAndEarnPeriod = ConstU32<10>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<100>; + type MaxNumberOfContracts = ConstU32<100>; type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU32<2>; From 978559bcd10dd82dd0cf7f52003931427d6e01a7 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 14 Nov 2023 09:49:42 +0100 Subject: [PATCH 57/86] Fix for cleanup --- pallets/dapp-staking-v3/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 22e0860a39..e3b6dd3fdd 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1485,6 +1485,7 @@ pub mod pallet { } }) .collect(); + let entries_to_delete = to_be_deleted.len(); // Remove all expired entries. for smart_contract in to_be_deleted { @@ -1494,6 +1495,7 @@ pub mod pallet { // Remove expired ledger stake entries, if needed. let threshold_period = Self::oldest_claimable_period(current_period); let mut ledger = Ledger::::get(&account); + ledger.contract_stake_count.saturating_reduce(entries_to_delete as u32); if ledger.maybe_cleanup_expired(threshold_period) { Self::update_ledger(&account, ledger); } From 416286605b1c333fc6ca0e6db4841da55f1ea349 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 14 Nov 2023 15:54:46 +0100 Subject: [PATCH 58/86] Minor fixes, more tests --- pallets/dapp-staking-v3/coverage.sh | 10 + pallets/dapp-staking-v3/src/lib.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 403 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 8 +- 4 files changed, 418 insertions(+), 7 deletions(-) create mode 100755 pallets/dapp-staking-v3/coverage.sh diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh new file mode 100755 index 0000000000..5fade398ea --- /dev/null +++ b/pallets/dapp-staking-v3/coverage.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +targets=("protocol_state" "account_ledger" "dapp_info" "period_info" "era_info" \ + "stake_amount" "singular_staking_info" "contract_stake_info" "era_reward_span" \ + "period_end_info") + +for target in "${targets[@]}" +do + cargo tarpaulin -p pallet-dapp-staking-v3 -o=html --output-dir=./coverage/$target -- $target +done \ No newline at end of file diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e3b6dd3fdd..4e2d7d8e57 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1495,7 +1495,9 @@ pub mod pallet { // Remove expired ledger stake entries, if needed. let threshold_period = Self::oldest_claimable_period(current_period); let mut ledger = Ledger::::get(&account); - ledger.contract_stake_count.saturating_reduce(entries_to_delete as u32); + ledger + .contract_stake_count + .saturating_reduce(entries_to_delete as u32); if ledger.maybe_cleanup_expired(threshold_period) { Self::update_ledger(&account, ledger); } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 34de7c96e0..e7812cabb5 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -162,6 +162,20 @@ fn dapp_info_basic_checks() { // Beneficiary receives rewards in case it is set dapp_info.reward_destination = Some(beneficiary); assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); + + // Check if dApp is active + assert!(dapp_info.is_active()); + + dapp_info.state = DAppState::Unregistered(10); + assert!(!dapp_info.is_active()); +} + +#[test] +fn unlocking_chunk_basic_check() { + // Sanity check + let unlocking_chunk = UnlockingChunk::::default(); + assert!(unlocking_chunk.amount.is_zero()); + assert!(unlocking_chunk.unlock_block.is_zero()); } #[test] @@ -1008,8 +1022,393 @@ fn account_ledger_consume_unlocking_chunks_works() { } #[test] -fn account_ledger_claim_up_to_era_works() { - // TODO!!! +fn account_ledger_expired_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + + // 1st scenario - nothing is expired + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: 100, + period: 5, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: 3, + build_and_earn: 13, + era: 101, + period: 5, + }); + + let acc_ledger_snapshot = acc_ledger.clone(); + + assert!(!acc_ledger.maybe_cleanup_expired(acc_ledger.staked.period - 1)); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "No change must happen since period hasn't expired." + ); + + assert!(!acc_ledger.maybe_cleanup_expired(acc_ledger.staked.period)); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "No change must happen since period hasn't expired." + ); + + // 2nd scenario - stake has expired + assert!(acc_ledger.maybe_cleanup_expired(acc_ledger.staked.period + 1)); + assert!(acc_ledger.staked.is_empty()); + assert!(acc_ledger.staked_future.is_none()); +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_without_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 100; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: stake_era, + period: 5, + }; + acc_ledger + }; + + // 1st scenario - claim one era, period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, None) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Only era should be bumped by 1." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 2nd scenario - claim multiple eras (5), period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, None) // staked era + 4 additional eras + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some((stake_era + inc, acc_ledger_snapshot.staked.total())) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 5; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Only era should be bumped by 5." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_with_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 100; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: stake_era, + period: 5, + }; + acc_ledger + }; + + // 1st scenario - claim one era, period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 2nd scenario - claim multiple eras (5), period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, Some(stake_era)) // staked era + 4 additional eras + .expect("Must provide iter with exactly one era."); + + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some((stake_era + inc, acc_ledger_snapshot.staked.total())) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 3rd scenario - claim one era, period has ended in some future era + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era + 1)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correctly updated + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Entry must exist since we still haven't reached the period end era." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_future_without_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 50; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked_future = Some(StakeAmount { + voting: 5, + build_and_earn: 11, + era: stake_era, + period: 4, + }); + acc_ledger + }; + + // 1st scenario - claim one era, period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, None) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some(( + stake_era, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Era must be bumped by 1, and entry must switch from staked_future over to staked." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future must be cleaned up after the claim." + ); + } + + // 2nd scenario - claim multiple eras (5), period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, None) // staked era + 4 additional eras + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some(( + stake_era + inc, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 5; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Era must be bumped by 5, and entry must switch from staked_future over to staked." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future must be cleaned up after the claim." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_future_with_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 50; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked_future = Some(StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }); + acc_ledger + }; + + // 1st scenario - claim one era, period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some(( + stake_era, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 2nd scenario - claim multiple eras (5), period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, Some(stake_era)) // staked era + 4 additional eras + .expect("Must provide iter with exactly one era."); + + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some(( + stake_era + inc, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 3rd scenario - claim one era, period has ended in some future era + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era + 1)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some(( + stake_era, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correctly updated + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Entry must exist since we still haven't reached the period end era." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } } #[test] diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 63b80c9867..59cbc62c7c 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -651,12 +651,12 @@ where /// Cleanup staking information if it has expired. /// /// # Args - /// `threshold_period` - last period for which entries can still be considered valid. + /// `valid_threshold_period` - last period for which entries can still be considered valid. /// /// `true` if any change was made, `false` otherwise. - pub fn maybe_cleanup_expired(&mut self, threshold_period: PeriodNumber) -> bool { + pub fn maybe_cleanup_expired(&mut self, valid_threshold_period: PeriodNumber) -> bool { match self.staked_period() { - Some(staked_period) if staked_period < threshold_period => { + Some(staked_period) if staked_period < valid_threshold_period => { self.staked = Default::default(); self.staked_future = None; true @@ -676,7 +676,7 @@ where period_end: Option, ) -> Result { // Main entry exists, but era isn't 'in history' - if !self.staked.is_empty() && era <= self.staked.era { + if !self.staked.is_empty() && era < self.staked.era { return Err(AccountLedgerError::NothingToClaim); } else if let Some(stake_amount) = self.staked_future { // Future entry exists, but era isn't 'in history' From 8060de59986143775ac7eae7d0b4ffc8adf32434 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 14 Nov 2023 17:49:25 +0100 Subject: [PATCH 59/86] More fixes & test --- pallets/dapp-staking-v3/coverage.sh | 2 +- .../dapp-staking-v3/src/test/tests_types.rs | 269 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 46 +-- 3 files changed, 294 insertions(+), 23 deletions(-) diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh index 5fade398ea..cd221a67e8 100755 --- a/pallets/dapp-staking-v3/coverage.sh +++ b/pallets/dapp-staking-v3/coverage.sh @@ -2,7 +2,7 @@ targets=("protocol_state" "account_ledger" "dapp_info" "period_info" "era_info" \ "stake_amount" "singular_staking_info" "contract_stake_info" "era_reward_span" \ - "period_end_info") + "period_end_info" "era_stake_pair_iter") for target in "${targets[@]}" do diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index e7812cabb5..91acc3a3a4 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1108,7 +1108,7 @@ fn account_ledger_claim_up_to_era_only_staked_without_cleanup_works() { let mut acc_ledger = acc_ledger_snapshot.clone(); let mut result_iter = acc_ledger .claim_up_to_era(stake_era + 4, None) // staked era + 4 additional eras - .expect("Must provide iter with exactly one era."); + .expect("Must provide iter with 5 values."); // Iter values are correct for inc in 0..5 { @@ -1179,7 +1179,7 @@ fn account_ledger_claim_up_to_era_only_staked_with_cleanup_works() { let mut acc_ledger = acc_ledger_snapshot.clone(); let mut result_iter = acc_ledger .claim_up_to_era(stake_era + 4, Some(stake_era)) // staked era + 4 additional eras - .expect("Must provide iter with exactly one era."); + .expect("Must provide iter with 5 values."); for inc in 0..5 { assert_eq!( @@ -1279,7 +1279,7 @@ fn account_ledger_claim_up_to_era_only_staked_future_without_cleanup_works() { let mut acc_ledger = acc_ledger_snapshot.clone(); let mut result_iter = acc_ledger .claim_up_to_era(stake_era + 4, None) // staked era + 4 additional eras - .expect("Must provide iter with exactly one era."); + .expect("Must provide iter with 5 entries."); // Iter values are correct for inc in 0..5 { @@ -1356,7 +1356,7 @@ fn account_ledger_claim_up_to_era_only_staked_future_with_cleanup_works() { let mut acc_ledger = acc_ledger_snapshot.clone(); let mut result_iter = acc_ledger .claim_up_to_era(stake_era + 4, Some(stake_era)) // staked era + 4 additional eras - .expect("Must provide iter with exactly one era."); + .expect("Must provide iter with 5 entries."); for inc in 0..5 { assert_eq!( @@ -1411,6 +1411,197 @@ fn account_ledger_claim_up_to_era_only_staked_future_with_cleanup_works() { } } +#[test] +fn account_ledger_claim_up_to_era_staked_and_staked_future_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era_1 = 100; + let stake_era_2 = stake_era_1 + 1; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: stake_era_1, + period: 5, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: 3, + build_and_earn: 11, + era: stake_era_2, + period: 5, + }); + acc_ledger + }; + + // 1st scenario - claim only one era + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era_1, None) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era_1, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, + acc_ledger_snapshot.staked_future.unwrap(), + "staked_future entry must be moved over to staked." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future is cleaned up since it's been moved over to staked entry." + ); + } + + // 2nd scenario - claim multiple eras (3), period hasn't ended yet, do the cleanup + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era_2 + 1, None) // staked era + 2 additional eras + .expect("Must provide iter with exactly two entries."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era_1, acc_ledger_snapshot.staked.total())) + ); + assert_eq!( + result_iter.next(), + Some(( + stake_era_2, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert_eq!( + result_iter.next(), + Some(( + stake_era_2 + 1, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 2; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "staked_future must move over to staked, and era must be incremented by 2." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future is cleaned up since it's been moved over to staked entry." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_fails_for_historic_eras() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 50; + + // Only staked entry + { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }; + assert_eq!( + acc_ledger.claim_up_to_era(stake_era - 1, None), + Err(AccountLedgerError::NothingToClaim) + ); + } + + // Only staked-future entry + { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked_future = Some(StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }); + assert_eq!( + acc_ledger.claim_up_to_era(stake_era - 1, None), + Err(AccountLedgerError::NothingToClaim) + ); + } + + // Both staked and staked-future entries + { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: 2, + build_and_earn: 19, + era: stake_era + 1, + period: 3, + }); + assert_eq!( + acc_ledger.claim_up_to_era(stake_era - 1, None), + Err(AccountLedgerError::NothingToClaim) + ); + } +} + +#[test] +fn era_stake_pair_iter_works() { + // 1st scenario - only span is given + let (first_era, last_era, amount) = (2, 5, 11); + let mut iter_1 = EraStakePairIter::new((first_era, last_era, amount), None).unwrap(); + for era in first_era..=last_era { + assert_eq!(iter_1.next(), Some((era, amount))); + } + assert!(iter_1.next().is_none()); + + // 2nd scenario - first value & span are given + let (maybe_first_era, maybe_first_amount) = (1, 7); + let maybe_first = Some((maybe_first_era, maybe_first_amount)); + let mut iter_2 = EraStakePairIter::new((first_era, last_era, amount), maybe_first).unwrap(); + + assert_eq!(iter_2.next(), Some((maybe_first_era, maybe_first_amount))); + for era in first_era..=last_era { + assert_eq!(iter_2.next(), Some((era, amount))); + } +} + +#[test] +fn era_stake_pair_iter_returns_error_for_illegal_data() { + // 1st scenario - spans are reversed; first era comes AFTER the last era + let (first_era, last_era, amount) = (2, 5, 11); + assert!(EraStakePairIter::new((last_era, first_era, amount), None).is_err()); + + // 2nd scenario - maybe_first covers the same era as the span + assert!(EraStakePairIter::new((first_era, last_era, amount), Some((first_era, 10))).is_err()); + + // 3rd scenario - maybe_first is before the span, but not exactly 1 era before the first era in the span + assert!( + EraStakePairIter::new((first_era, last_era, amount), Some((first_era - 2, 10))).is_err() + ); + + assert!( + EraStakePairIter::new((first_era, last_era, amount), Some((first_era - 1, 10))).is_ok(), + "Sanity check." + ); +} + #[test] fn era_info_lock_unlock_works() { let mut era_info = EraInfo::default(); @@ -1569,6 +1760,76 @@ fn era_info_unstake_works() { .is_zero()); } +#[test] +fn era_info_migrate_to_next_era_works() { + // Make dummy era info with stake amounts + let era_info_snapshot = EraInfo { + active_era_locked: 123, + total_locked: 456, + unlocking: 13, + current_stake_amount: StakeAmount { + voting: 13, + build_and_earn: 29, + era: 2, + period: 1, + }, + next_stake_amount: StakeAmount { + voting: 13, + build_and_earn: 41, + era: 3, + period: 1, + }, + }; + + // 1st scenario - rollover to next era, no subperiod change + { + let mut era_info = era_info_snapshot; + era_info.migrate_to_next_era(None); + + assert_eq!(era_info.active_era_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); + assert_eq!( + era_info.current_stake_amount, + era_info_snapshot.next_stake_amount + ); + + let mut new_next_stake_amount = era_info_snapshot.next_stake_amount; + new_next_stake_amount.era += 1; + assert_eq!(era_info.next_stake_amount, new_next_stake_amount); + } + + // 2nd scenario - rollover to next era, change from Voting into Build&Earn subperiod + { + let mut era_info = era_info_snapshot; + era_info.migrate_to_next_era(Some(Subperiod::BuildAndEarn)); + + assert_eq!(era_info.active_era_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); + assert_eq!( + era_info.current_stake_amount, + era_info_snapshot.next_stake_amount + ); + + let mut new_next_stake_amount = era_info_snapshot.next_stake_amount; + new_next_stake_amount.era += 1; + assert_eq!(era_info.next_stake_amount, new_next_stake_amount); + } + + // 3rd scenario - rollover to next era, change from Build&Earn to Voting subperiod + { + let mut era_info = era_info_snapshot; + era_info.migrate_to_next_era(Some(Subperiod::Voting)); + + assert_eq!(era_info.active_era_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); + assert!(era_info.current_stake_amount.is_empty()); + assert!(era_info.next_stake_amount.is_empty()); + } +} + #[test] fn stake_amount_works() { let mut stake_amount = StakeAmount::default(); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 59cbc62c7c..605d499f19 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -112,6 +112,8 @@ pub enum AccountLedgerError { NothingToClaim, /// Rewards have already been claimed AlreadyClaimed, + /// Attempt to crate the iterator failed due to incorrect data. + InvalidIterator, } /// Distinct subperiods in dApp staking protocol. @@ -676,22 +678,23 @@ where period_end: Option, ) -> Result { // Main entry exists, but era isn't 'in history' - if !self.staked.is_empty() && era < self.staked.era { - return Err(AccountLedgerError::NothingToClaim); + if !self.staked.is_empty() { + ensure!(era >= self.staked.era, AccountLedgerError::NothingToClaim); } else if let Some(stake_amount) = self.staked_future { // Future entry exists, but era isn't 'in history' - if era < stake_amount.era { - return Err(AccountLedgerError::NothingToClaim); - } + ensure!(era >= stake_amount.era, AccountLedgerError::NothingToClaim); } // There are multiple options: // 1. We only have future entry, no current entry - // 2. We have both current and future entry - // 3. We only have current entry, no future entry + // 2. We have both current and future entry, but are only claiming 1 era + // 3. We have both current and future entry, and are claiming multiple eras + // 4. We only have current entry, no future entry let (span, maybe_first) = if let Some(stake_amount) = self.staked_future { if self.staked.is_empty() { ((stake_amount.era, era, stake_amount.total()), None) + } else if self.staked.era == era { + ((era, era, self.staked.total()), None) } else { ( (stake_amount.era, era, stake_amount.total()), @@ -702,7 +705,9 @@ where ((self.staked.era, era, self.staked.total()), None) }; - let result = EraStakePairIter::new(span, maybe_first); + println!("span: {:?}, maybe_first: {:?}", span, maybe_first); + let result = EraStakePairIter::new(span, maybe_first) + .map_err(|_| AccountLedgerError::InvalidIterator)?; // Rollover future to 'current' stake amount if let Some(stake_amount) = self.staked_future.take() { @@ -750,20 +755,25 @@ impl EraStakePairIter { pub fn new( span: (EraNumber, EraNumber, Balance), maybe_first: Option<(EraNumber, Balance)>, - ) -> Self { - if let Some((era, _amount)) = maybe_first { - debug_assert!( - span.0 == era + 1, - "The 'other', if it exists, must cover era preceding the span." - ); + ) -> Result { + // First era must be smaller or equal to the last era. + if span.0 > span.1 { + return Err(()); + } + // If 'maybe_first' is defined, it must exactly match the `span.0 - 1` era value. + match maybe_first { + Some((era, _)) if span.0.saturating_sub(era) != 1 => { + return Err(()); + } + _ => (), } - Self { + Ok(Self { maybe_first, start_era: span.0, end_era: span.1, amount: span.2, - } + }) } } @@ -872,12 +882,11 @@ impl StakeAmount { /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { + // TODO: can some of these values be cleaned up? We no longer need to keep track of two separate lock values? /// How much balance is considered to be locked in the current era. - /// This value influences the reward distribution. #[codec(compact)] pub active_era_locked: Balance, /// How much balance is locked in dApp staking, in total. - /// For rewards, this amount isn't relevant for the current era, but only from the next one. #[codec(compact)] pub total_locked: Balance, /// How much balance is undergoing unlocking process. @@ -953,6 +962,7 @@ impl EraInfo { } Some(Subperiod::BuildAndEarn) | None => { self.current_stake_amount = self.next_stake_amount; + self.next_stake_amount.era.saturating_inc(); } }; } From 7f81f32e458aca9e821f21b91e711088c8ae3203 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 15 Nov 2023 08:07:41 +0100 Subject: [PATCH 60/86] Minor changes --- pallets/dapp-staking-v3/coverage.sh | 2 +- pallets/dapp-staking-v3/src/lib.rs | 22 +-- .../dapp-staking-v3/src/test/testing_utils.rs | 11 -- .../dapp-staking-v3/src/test/tests_types.rs | 159 +++++++++++++----- pallets/dapp-staking-v3/src/types.rs | 67 ++++---- 5 files changed, 163 insertions(+), 98 deletions(-) diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh index cd221a67e8..e0b6128b56 100755 --- a/pallets/dapp-staking-v3/coverage.sh +++ b/pallets/dapp-staking-v3/coverage.sh @@ -1,7 +1,7 @@ #!/bin/sh targets=("protocol_state" "account_ledger" "dapp_info" "period_info" "era_info" \ - "stake_amount" "singular_staking_info" "contract_stake_info" "era_reward_span" \ + "stake_amount" "singular_staking_info" "contract_stake_amount" "era_reward_span" \ "period_end_info" "era_stake_pair_iter") for target in "${targets[@]}" diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 4e2d7d8e57..e9ca6e7c87 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -990,11 +990,11 @@ pub mod pallet { ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); - let stake_era = protocol_state.era; + let current_era = protocol_state.era; ensure!( !protocol_state .period_info - .is_next_period(stake_era.saturating_add(1)), + .is_next_period(current_era.saturating_add(1)), Error::::PeriodEndsInNextEra ); @@ -1004,7 +1004,7 @@ pub mod pallet { // 1. // Increase stake amount for the next era & current period in staker's ledger ledger - .add_stake_amount(amount, stake_era, protocol_state.period_info) + .add_stake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewardsFromPastPeriods @@ -1047,7 +1047,7 @@ pub mod pallet { true, ), }; - new_staking_info.stake(amount, protocol_state.subperiod()); + new_staking_info.stake(amount, current_era, protocol_state.subperiod()); ensure!( new_staking_info.total_staked_amount() >= T::MinimumStakeAmount::get(), Error::::InsufficientStakeAmount @@ -1064,7 +1064,7 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the new stake amount on the specified contract. let mut contract_stake_info = ContractStake::::get(&dapp_info.id); - contract_stake_info.stake(amount, protocol_state.period_info, stake_era); + contract_stake_info.stake(amount, protocol_state.period_info, current_era); // 4. // Update total staked amount for the next era. @@ -1108,7 +1108,7 @@ pub mod pallet { ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); - let unstake_era = protocol_state.era; + let current_era = protocol_state.era; let mut ledger = Ledger::::get(&account); @@ -1135,7 +1135,7 @@ pub mod pallet { amount }; - staking_info.unstake(amount, protocol_state.subperiod()); + staking_info.unstake(amount, current_era, protocol_state.subperiod()); (staking_info, amount) } None => { @@ -1146,7 +1146,7 @@ pub mod pallet { // 2. // Reduce stake amount ledger - .unstake_amount(amount, unstake_era, protocol_state.period_info) + .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { @@ -1161,7 +1161,7 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the reduced stake amount on the specified contract. let mut contract_stake_info = ContractStake::::get(&dapp_info.id); - contract_stake_info.unstake(amount, protocol_state.period_info, unstake_era); + contract_stake_info.unstake(amount, protocol_state.period_info, current_era); // 4. // Update total staked amount for the next era. @@ -1415,7 +1415,7 @@ pub mod pallet { ); let protocol_state = ActiveProtocolState::::get(); - let unstake_era = protocol_state.era; + let current_era = protocol_state.era; // Extract total staked amount on the specified unregistered contract let amount = match StakerInfo::::get(&account, &smart_contract) { @@ -1435,7 +1435,7 @@ pub mod pallet { // Reduce stake amount in ledger let mut ledger = Ledger::::get(&account); ledger - .unstake_amount(amount, unstake_era, protocol_state.period_info) + .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 0d66143d84..e2203397a3 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -225,11 +225,6 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { pre_snapshot.current_era_info.total_locked + expected_lock_amount, "Total locked balance should be increased by the amount locked." ); - assert_eq!( - post_snapshot.current_era_info.active_era_locked, - pre_snapshot.current_era_info.active_era_locked, - "Active era locked amount should remain exactly the same." - ); } /// Start the unlocking process for locked funds and assert success. @@ -311,12 +306,6 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { .saturating_sub(expected_unlock_amount), post_era_info.total_locked ); - assert_eq!( - pre_era_info - .active_era_locked - .saturating_sub(expected_unlock_amount), - post_era_info.active_era_locked - ); } /// Claims the unlocked funds back into free balance of the user and assert success. diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 91acc3a3a4..7251f53be9 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -327,7 +327,12 @@ fn account_ledger_staked_amount_works() { // Period matches let amount_1 = 29; let period = 5; - acc_ledger.staked = StakeAmount::new(amount_1, 0, 1, period); + acc_ledger.staked = StakeAmount { + voting: amount_1, + build_and_earn: 0, + era: 1, + period: period, + }; assert_eq!(acc_ledger.staked_amount(period), amount_1); // Period doesn't match @@ -336,7 +341,12 @@ fn account_ledger_staked_amount_works() { // Add future entry let amount_2 = 17; - acc_ledger.staked_future = Some(StakeAmount::new(0, amount_2, 2, period)); + acc_ledger.staked_future = Some(StakeAmount { + voting: 0, + build_and_earn: amount_2, + era: 2, + period: period, + }); assert_eq!(acc_ledger.staked_amount(period), amount_2); assert!(acc_ledger.staked_amount(period - 1).is_zero()); assert!(acc_ledger.staked_amount(period + 1).is_zero()); @@ -424,7 +434,12 @@ fn account_ledger_stakeable_amount_works() { // Second scenario - some staked amount is introduced, period is still valid let first_era = 1; let staked_amount = 7; - acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, period_1); + acc_ledger.staked = StakeAmount { + voting: 0, + build_and_earn: staked_amount, + era: first_era, + period: period_1, + }; assert_eq!( acc_ledger.stakeable_amount(period_1), @@ -585,7 +600,12 @@ fn account_ledger_add_stake_amount_advanced_example_works() { acc_ledger.add_lock_amount(lock_amount); // We only have entry for the current era - acc_ledger.staked = StakeAmount::new(stake_amount_1, 0, first_era, period_1); + acc_ledger.staked = StakeAmount { + voting: stake_amount_1, + build_and_earn: 0, + era: first_era, + period: period_1, + }; let stake_amount_2 = 2; let acc_ledger_snapshot = acc_ledger.clone(); @@ -655,7 +675,12 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { ); // Alternative situation - no future entry, only current era - acc_ledger.staked = StakeAmount::new(0, stake_amount, first_era, period_1); + acc_ledger.staked = StakeAmount { + voting: 0, + build_and_earn: stake_amount, + era: first_era, + period: period_1, + }; acc_ledger.staked_future = None; assert_eq!( @@ -744,7 +769,12 @@ fn account_ledger_unstake_amount_basic_scenario_works() { .is_ok()); // Only 'current' entry has some values, future is set to None. - acc_ledger_2.staked = StakeAmount::new(0, amount_1, era_1, period_1); + acc_ledger_2.staked = StakeAmount { + voting: 0, + build_and_earn: amount_1, + era: era_1, + period: period_1, + }; acc_ledger_2.staked_future = None; for mut acc_ledger in vec![acc_ledger, acc_ledger_2] { @@ -788,8 +818,18 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { acc_ledger.add_lock_amount(amount_1); // We have two entries at once - acc_ledger.staked = StakeAmount::new(amount_1 - 1, 0, era_1, period_1); - acc_ledger.staked_future = Some(StakeAmount::new(amount_1 - 1, 1, era_1 + 1, period_1)); + acc_ledger.staked = StakeAmount { + voting: amount_1 - 1, + build_and_earn: 0, + era: era_1, + period: period_1, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: amount_1 - 1, + build_and_earn: 1, + era: era_1 + 1, + period: period_1, + }); // 1st scenario - unstake some amount from the current era, both entries should be affected. let unstake_amount_1 = 3; @@ -883,7 +923,12 @@ fn account_ledger_unstake_from_invalid_era_fails() { ); // Alternative situation - no future entry, only current era - acc_ledger.staked = StakeAmount::new(0, 1, era_1, period_1); + acc_ledger.staked = StakeAmount { + voting: 0, + build_and_earn: 1, + era: era_1, + period: period_1, + }; acc_ledger.staked_future = None; assert_eq!( @@ -1608,7 +1653,6 @@ fn era_info_lock_unlock_works() { // Sanity check assert!(era_info.total_locked.is_zero()); - assert!(era_info.active_era_locked.is_zero()); assert!(era_info.unlocking.is_zero()); // Basic add lock @@ -1621,7 +1665,6 @@ fn era_info_lock_unlock_works() { // Basic unlocking started let unlock_amount = 2; era_info.total_locked = 17; - era_info.active_era_locked = 13; let era_info_snapshot = era_info; // First unlock & checks @@ -1630,10 +1673,6 @@ fn era_info_lock_unlock_works() { era_info.total_locked, era_info_snapshot.total_locked - unlock_amount ); - assert_eq!( - era_info.active_era_locked, - era_info_snapshot.active_era_locked - unlock_amount - ); assert_eq!(era_info.unlocking, unlock_amount); // Second unlock and checks @@ -1642,17 +1681,13 @@ fn era_info_lock_unlock_works() { era_info.total_locked, era_info_snapshot.total_locked - unlock_amount * 2 ); - assert_eq!( - era_info.active_era_locked, - era_info_snapshot.active_era_locked - unlock_amount * 2 - ); assert_eq!(era_info.unlocking, unlock_amount * 2); // Claim unlocked chunks let old_era_info = era_info.clone(); era_info.unlocking_removed(1); assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); - assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); + assert_eq!(era_info.total_locked, old_era_info.total_locked); } #[test] @@ -1702,10 +1737,18 @@ fn era_info_unstake_works() { let bep_stake_amount_2 = bep_stake_amount_1 + 6; let period_number = 1; let era = 2; - era_info.current_stake_amount = - StakeAmount::new(vp_stake_amount, bep_stake_amount_1, era, period_number); - era_info.next_stake_amount = - StakeAmount::new(vp_stake_amount, bep_stake_amount_2, era + 1, period_number); + era_info.current_stake_amount = StakeAmount { + voting: vp_stake_amount, + build_and_earn: bep_stake_amount_1, + era: era, + period: period_number, + }; + era_info.next_stake_amount = StakeAmount { + voting: vp_stake_amount, + build_and_earn: bep_stake_amount_2, + era: era + 1, + period: period_number, + }; let total_staked = era_info.total_staked_amount(); let total_staked_next_era = era_info.total_staked_amount_next_era(); @@ -1764,7 +1807,6 @@ fn era_info_unstake_works() { fn era_info_migrate_to_next_era_works() { // Make dummy era info with stake amounts let era_info_snapshot = EraInfo { - active_era_locked: 123, total_locked: 456, unlocking: 13, current_stake_amount: StakeAmount { @@ -1786,7 +1828,6 @@ fn era_info_migrate_to_next_era_works() { let mut era_info = era_info_snapshot; era_info.migrate_to_next_era(None); - assert_eq!(era_info.active_era_locked, era_info_snapshot.total_locked); assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); assert_eq!( @@ -1804,7 +1845,6 @@ fn era_info_migrate_to_next_era_works() { let mut era_info = era_info_snapshot; era_info.migrate_to_next_era(Some(Subperiod::BuildAndEarn)); - assert_eq!(era_info.active_era_locked, era_info_snapshot.total_locked); assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); assert_eq!( @@ -1822,7 +1862,6 @@ fn era_info_migrate_to_next_era_works() { let mut era_info = era_info_snapshot; era_info.migrate_to_next_era(Some(Subperiod::Voting)); - assert_eq!(era_info.active_era_locked, era_info_snapshot.total_locked); assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); assert!(era_info.current_stake_amount.is_empty()); @@ -1904,11 +1943,14 @@ fn singular_staking_info_basics_are_ok() { assert_eq!(staking_info.period_number(), period_number); assert!(staking_info.is_loyal()); assert!(staking_info.total_staked_amount().is_zero()); + assert!(staking_info.is_empty()); + assert!(staking_info.era().is_zero()); assert!(!SingularStakingInfo::new(period_number, Subperiod::BuildAndEarn).is_loyal()); // Add some staked amount during `Voting` period + let era_1 = 7; let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, Subperiod::Voting); + staking_info.stake(vote_stake_amount_1, era_1, Subperiod::Voting); assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); assert_eq!( staking_info.staked_amount(Subperiod::Voting), @@ -1917,10 +1959,16 @@ fn singular_staking_info_basics_are_ok() { assert!(staking_info .staked_amount(Subperiod::BuildAndEarn) .is_zero()); + assert_eq!( + staking_info.era(), + era_1 + 1, + "Stake era should remain valid." + ); // Add some staked amount during `BuildAndEarn` period + let era_2 = 9; let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, era_2, Subperiod::BuildAndEarn); assert_eq!( staking_info.total_staked_amount(), vote_stake_amount_1 + bep_stake_amount_1 @@ -1933,6 +1981,7 @@ fn singular_staking_info_basics_are_ok() { staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 ); + assert_eq!(staking_info.era(), era_2 + 1); } #[test] @@ -1942,13 +1991,14 @@ fn singular_staking_info_unstake_during_voting_is_ok() { let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions + let era_1 = 2; let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, Subperiod::Voting); + staking_info.stake(vote_stake_amount_1, era_1, Subperiod::Voting); // Unstake some amount during `Voting` period, loyalty should remain as expected. let unstake_amount_1 = 5; assert_eq!( - staking_info.unstake(unstake_amount_1, Subperiod::Voting), + staking_info.unstake(unstake_amount_1, era_1, Subperiod::Voting), (unstake_amount_1, Balance::zero()) ); assert_eq!( @@ -1956,15 +2006,22 @@ fn singular_staking_info_unstake_during_voting_is_ok() { vote_stake_amount_1 - unstake_amount_1 ); assert!(staking_info.is_loyal()); + assert_eq!( + staking_info.era(), + era_1 + 1, + "Stake era should remain valid." + ); // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. + let era_2 = era_1 + 2; let remaining_stake = staking_info.total_staked_amount(); assert_eq!( - staking_info.unstake(remaining_stake + 1, Subperiod::Voting), + staking_info.unstake(remaining_stake + 1, era_2, Subperiod::Voting), (remaining_stake, Balance::zero()) ); assert!(staking_info.total_staked_amount().is_zero()); assert!(staking_info.is_loyal()); + assert_eq!(staking_info.era(), era_2); } #[test] @@ -1974,15 +2031,16 @@ fn singular_staking_info_unstake_during_bep_is_ok() { let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions + let era_1 = 3; let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, Subperiod::Voting); + staking_info.stake(vote_stake_amount_1, era_1 - 1, Subperiod::Voting); let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, era_1, Subperiod::BuildAndEarn); // 1st scenario - Unstake some of the amount staked during B&E period let unstake_1 = 5; assert_eq!( - staking_info.unstake(5, Subperiod::BuildAndEarn), + staking_info.unstake(5, era_1, Subperiod::BuildAndEarn), (Balance::zero(), unstake_1) ); assert_eq!( @@ -1998,6 +2056,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { bep_stake_amount_1 - unstake_1 ); assert!(staking_info.is_loyal()); + assert_eq!( + staking_info.era(), + era_1 + 1, + "Stake era should remain valid." + ); // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. // The point is to take a chunk from the voting period stake too. @@ -2005,9 +2068,10 @@ fn singular_staking_info_unstake_during_bep_is_ok() { let current_bep_stake = staking_info.staked_amount(Subperiod::BuildAndEarn); let voting_stake_overflow = 2; let unstake_2 = current_bep_stake + voting_stake_overflow; + let era_2 = era_1 + 3; assert_eq!( - staking_info.unstake(unstake_2, Subperiod::BuildAndEarn), + staking_info.unstake(unstake_2, era_2, Subperiod::BuildAndEarn), (voting_stake_overflow, current_bep_stake) ); assert_eq!( @@ -2025,12 +2089,23 @@ fn singular_staking_info_unstake_during_bep_is_ok() { !staking_info.is_loyal(), "Loyalty flag should have been removed due to non-zero voting period unstake" ); + assert_eq!(staking_info.era(), era_2); } #[test] -fn contract_stake_info_get_works() { - let info_1 = StakeAmount::new(0, 0, 4, 2); - let info_2 = StakeAmount::new(11, 0, 7, 3); +fn contract_stake_amount_get_works() { + let info_1 = StakeAmount { + voting: 0, + build_and_earn: 0, + era: 4, + period: 2, + }; + let info_2 = StakeAmount { + voting: 11, + build_and_earn: 0, + era: 7, + period: 3, + }; let contract_stake = ContractStakeAmount { staked: info_1, @@ -2068,7 +2143,7 @@ fn contract_stake_info_get_works() { } #[test] -fn contract_stake_info_stake_is_ok() { +fn contract_stake_amount_stake_is_ok() { let mut contract_stake = ContractStakeAmount::default(); // 1st scenario - stake some amount and verify state change @@ -2165,7 +2240,7 @@ fn contract_stake_info_stake_is_ok() { } #[test] -fn contract_stake_info_unstake_is_ok() { +fn contract_stake_amount_unstake_is_ok() { let mut contract_stake = ContractStakeAmount::default(); // Prep action - create a stake entry diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 605d499f19..7a9e0c6174 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -141,7 +141,7 @@ pub struct PeriodInfo { /// Period number. #[codec(compact)] pub number: PeriodNumber, - /// subperiod. + /// Subperiod ytpe. pub subperiod: Subperiod, /// Last era of the subperiod, after this a new subperiod should start. #[codec(compact)] @@ -150,7 +150,7 @@ pub struct PeriodInfo { impl PeriodInfo { /// `true` if the provided era belongs to the next period, `false` otherwise. - /// It's only possible to provide this information for the `BuildAndEarn` subperiod. + /// It's only possible to provide this information correctly for the ongoing `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { self.subperiod == Subperiod::BuildAndEarn && self.subperiod_end_era <= era } @@ -231,7 +231,7 @@ where self.period_info.subperiod_end_era } - /// Checks whether a new era should be triggered, based on the provided `BlockNumber` argument + /// Checks whether a new era should be triggered, based on the provided _current_ block number argument /// or possibly other protocol state parameters. pub fn is_new_era(&self, now: BlockNumber) -> bool { self.next_era_start <= now @@ -337,10 +337,10 @@ pub struct AccountLedger< BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, > { - /// How much active locked amount an account has. + /// How much active locked amount an account has. This can be used for staking. #[codec(compact)] pub locked: Balance, - /// Vector of all the unlocking chunks. + /// Vector of all the unlocking chunks. This is also considered _locked_ but cannot be used for staking. pub unlocking: BoundedVec, UnlockingLen>, /// Primary field used to store how much was staked in a particular era. pub staked: StakeAmount, @@ -705,7 +705,6 @@ where ((self.staked.era, era, self.staked.total()), None) }; - println!("span: {:?}, maybe_first: {:?}", span, maybe_first); let result = EraStakePairIter::new(span, maybe_first) .map_err(|_| AccountLedgerError::InvalidIterator)?; @@ -717,7 +716,7 @@ where // Make sure to clean up the entries if all rewards for the period have been claimed. match period_end { - Some(subperiod_end_era) if era >= subperiod_end_era => { + Some(period_end_era) if era >= period_end_era => { self.staked = Default::default(); self.staked_future = None; } @@ -815,21 +814,6 @@ pub struct StakeAmount { } impl StakeAmount { - /// Create new instance of `StakeAmount` with specified `voting` and `build_and_earn` amounts. - pub fn new( - voting: Balance, - build_and_earn: Balance, - era: EraNumber, - period: PeriodNumber, - ) -> Self { - Self { - voting, - build_and_earn, - era, - period, - } - } - /// `true` if nothing is staked, `false` otherwise pub fn is_empty(&self) -> bool { self.voting.is_zero() && self.build_and_earn.is_zero() @@ -870,6 +854,10 @@ impl StakeAmount { self.build_and_earn.saturating_reduce(amount); } else { // Rollover from build&earn to voting, is guaranteed to be larger than zero due to previous check + // E.g. voting = 10, build&earn = 5, amount = 7 + // underflow = build&earn - amount = 5 - 7 = -2 + // voting = 10 - 2 = 8 + // build&earn = 0 let remainder = amount.saturating_sub(self.build_and_earn); self.build_and_earn = Balance::zero(); self.voting.saturating_reduce(remainder); @@ -882,11 +870,8 @@ impl StakeAmount { /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { - // TODO: can some of these values be cleaned up? We no longer need to keep track of two separate lock values? - /// How much balance is considered to be locked in the current era. - #[codec(compact)] - pub active_era_locked: Balance, - /// How much balance is locked in dApp staking, in total. + /// How much balance is locked in dApp staking. + /// Does not include the amount that is undergoing the unlocking period. #[codec(compact)] pub total_locked: Balance, /// How much balance is undergoing unlocking process. @@ -907,7 +892,6 @@ impl EraInfo { /// Update with the new amount that has just started undergoing the unlocking period. pub fn unlocking_started(&mut self, amount: Balance) { - self.active_era_locked.saturating_reduce(amount); self.total_locked.saturating_reduce(amount); self.unlocking.saturating_accrue(amount); } @@ -953,7 +937,6 @@ impl EraInfo { /// ## Args /// `next_subperiod` - `None` if no subperiod change, `Some(type)` if `type` is starting from the next era. pub fn migrate_to_next_era(&mut self, next_subperiod: Option) { - self.active_era_locked = self.total_locked; match next_subperiod { // If next era marks start of new voting period period, it means we're entering a new period Some(Subperiod::Voting) => { @@ -988,16 +971,22 @@ impl SingularStakingInfo { /// `subperiod` - subperiod during which this entry is created. pub fn new(period: PeriodNumber, subperiod: Subperiod) -> Self { Self { - staked: StakeAmount::new(Balance::zero(), Balance::zero(), 0, period), + staked: StakeAmount { + voting: Balance::zero(), + build_and_earn: Balance::zero(), + era: 0, + period: period, + }, // Loyalty staking is only possible if stake is first made during the voting period. loyal_staker: subperiod == Subperiod::Voting, } } /// Stake the specified amount on the contract, for the specified subperiod. - pub fn stake(&mut self, amount: Balance, subperiod: Subperiod) { - // TODO: if we keep `StakeAmount` type here, consider including the era as well for consistency + pub fn stake(&mut self, amount: Balance, current_era: EraNumber, subperiod: Subperiod) { self.staked.add(amount, subperiod); + // Stake is only valid from the next era so we keep it consistent here + self.staked.era = current_era.saturating_add(1); } /// Unstakes some of the specified amount from the contract. @@ -1006,10 +995,17 @@ impl SingularStakingInfo { /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. /// /// Returns the amount that was unstaked from the `voting period` stake, and from the `build&earn period` stake. - pub fn unstake(&mut self, amount: Balance, subperiod: Subperiod) -> (Balance, Balance) { + pub fn unstake( + &mut self, + amount: Balance, + current_era: EraNumber, + subperiod: Subperiod, + ) -> (Balance, Balance) { let snapshot = self.staked; self.staked.subtract(amount, subperiod); + // Keep the latest era for which the entry is valid + self.staked.era = self.staked.era.max(current_era); self.loyal_staker = self.loyal_staker && (subperiod == Subperiod::Voting @@ -1044,6 +1040,11 @@ impl SingularStakingInfo { self.staked.period } + /// Era in which the entry was last time updated + pub fn era(&self) -> EraNumber { + self.staked.era + } + /// `true` if no stake exists, `false` otherwise. pub fn is_empty(&self) -> bool { self.staked.is_empty() From 4e85bdbb422551b208d42d43a07540e1220210b2 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 15 Nov 2023 09:04:35 +0100 Subject: [PATCH 61/86] More tests --- .../dapp-staking-v3/src/test/tests_types.rs | 132 +++++++++++++----- pallets/dapp-staking-v3/src/types.rs | 2 +- 2 files changed, 101 insertions(+), 33 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 7251f53be9..df60c6bf46 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -331,7 +331,7 @@ fn account_ledger_staked_amount_works() { voting: amount_1, build_and_earn: 0, era: 1, - period: period, + period, }; assert_eq!(acc_ledger.staked_amount(period), amount_1); @@ -345,7 +345,7 @@ fn account_ledger_staked_amount_works() { voting: 0, build_and_earn: amount_2, era: 2, - period: period, + period, }); assert_eq!(acc_ledger.staked_amount(period), amount_2); assert!(acc_ledger.staked_amount(period - 1).is_zero()); @@ -1740,7 +1740,7 @@ fn era_info_unstake_works() { era_info.current_stake_amount = StakeAmount { voting: vp_stake_amount, build_and_earn: bep_stake_amount_1, - era: era, + era, period: period_number, }; era_info.next_stake_amount = StakeAmount { @@ -2093,53 +2093,121 @@ fn singular_staking_info_unstake_during_bep_is_ok() { } #[test] -fn contract_stake_amount_get_works() { - let info_1 = StakeAmount { - voting: 0, - build_and_earn: 0, - era: 4, - period: 2, +fn contract_stake_amount_basic_get_checks_work() { + // Sanity checks for empty struct + let contract_stake = ContractStakeAmount { + staked: Default::default(), + staked_future: None, + tier_label: None, }; - let info_2 = StakeAmount { + assert!(contract_stake.is_empty()); + assert!(contract_stake.latest_stake_period().is_none()); + assert!(contract_stake.latest_stake_era().is_none()); + assert!(contract_stake.total_staked_amount(0).is_zero()); + assert!(contract_stake.staked_amount(0, Subperiod::Voting).is_zero()); + assert!(contract_stake + .staked_amount(0, Subperiod::BuildAndEarn) + .is_zero()); + + let era = 3; + let period = 2; + let amount = StakeAmount { + voting: 11, + build_and_earn: 17, + era, + period, + }; + let contract_stake = ContractStakeAmount { + staked: amount, + staked_future: None, + tier_label: None, + }; + assert!(!contract_stake.is_empty()); + + // Checks for illegal periods + for illegal_period in [period - 1, period + 1] { + assert!(contract_stake.total_staked_amount(illegal_period).is_zero()); + assert!(contract_stake + .staked_amount(illegal_period, Subperiod::Voting) + .is_zero()); + assert!(contract_stake + .staked_amount(illegal_period, Subperiod::BuildAndEarn) + .is_zero()); + } + + // Check for the valid period + assert_eq!(contract_stake.latest_stake_period(), Some(period)); + assert_eq!(contract_stake.latest_stake_era(), Some(era)); + assert_eq!(contract_stake.total_staked_amount(period), amount.total()); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::Voting), + amount.voting + ); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::BuildAndEarn), + amount.build_and_earn + ); +} + +#[test] +fn contract_stake_amount_advanced_get_checks_work() { + let (era_1, era_2) = (4, 7); + let period = 2; + let amount_1 = StakeAmount { voting: 11, build_and_earn: 0, - era: 7, - period: 3, + era: era_1, + period, + }; + let amount_2 = StakeAmount { + voting: 11, + build_and_earn: 13, + era: era_2, + period, }; let contract_stake = ContractStakeAmount { - staked: info_1, - staked_future: Some(info_2), + staked: amount_1, + staked_future: Some(amount_2), tier_label: None, }; - // Sanity check + // Sanity checks - all values from the 'future' entry should be relevant assert!(!contract_stake.is_empty()); + assert_eq!(contract_stake.latest_stake_period(), Some(period)); + assert_eq!(contract_stake.latest_stake_era(), Some(era_2)); + assert_eq!(contract_stake.total_staked_amount(period), amount_2.total()); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::Voting), + amount_2.voting + ); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::BuildAndEarn), + amount_2.build_and_earn + ); // 1st scenario - get existing entries - assert_eq!(contract_stake.get(4, 2), Some(info_1)); - assert_eq!(contract_stake.get(7, 3), Some(info_2)); + assert_eq!(contract_stake.get(era_1, period), Some(amount_1)); + assert_eq!(contract_stake.get(era_2, period), Some(amount_2)); // 2nd scenario - get non-existing entries for covered eras - { - let era_1 = 6; - let entry_1 = contract_stake.get(era_1, 2).expect("Has to be Some"); - assert!(entry_1.total().is_zero()); - assert_eq!(entry_1.era, era_1); - assert_eq!(entry_1.period, 2); - - let era_2 = 8; - let entry_1 = contract_stake.get(era_2, 3).expect("Has to be Some"); - assert_eq!(entry_1.total(), 11); - assert_eq!(entry_1.era, era_2); - assert_eq!(entry_1.period, 3); - } + let era_3 = era_2 - 1; + let entry_1 = contract_stake.get(era_3, 2).expect("Has to be Some"); + assert_eq!(entry_1.total(), amount_1.total()); + assert_eq!(entry_1.era, era_3); + assert_eq!(entry_1.period, period); + + let era_4 = era_2 + 1; + let entry_1 = contract_stake.get(era_4, period).expect("Has to be Some"); + assert_eq!(entry_1.total(), amount_2.total()); + assert_eq!(entry_1.era, era_4); + assert_eq!(entry_1.period, period); // 3rd scenario - get non-existing entries for covered eras but mismatching period - assert!(contract_stake.get(8, 2).is_none()); + assert!(contract_stake.get(8, period + 1).is_none()); // 4th scenario - get non-existing entries for non-covered eras - assert!(contract_stake.get(3, 2).is_none()); + assert!(contract_stake.get(3, period).is_none()); } #[test] diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 7a9e0c6174..0fef9d4a99 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -975,7 +975,7 @@ impl SingularStakingInfo { voting: Balance::zero(), build_and_earn: Balance::zero(), era: 0, - period: period, + period, }, // Loyalty staking is only possible if stake is first made during the voting period. loyal_staker: subperiod == Subperiod::Voting, From f8d52970433b867c6d13fdc571d0ac7961d042b0 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 15 Nov 2023 10:05:42 +0100 Subject: [PATCH 62/86] More tests, some clippy fixes --- .../dapp-staking-v3/src/test/tests_types.rs | 84 +++++++++++++++++-- pallets/dapp-staking-v3/src/types.rs | 3 - runtime/local/src/lib.rs | 2 +- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index df60c6bf46..5244ed5476 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -2226,6 +2226,11 @@ fn contract_stake_amount_stake_is_ok() { let amount_1 = 31; contract_stake.stake(amount_1, period_info_1, era_1); assert!(!contract_stake.is_empty()); + assert!( + contract_stake.staked.is_empty(), + "Only future entry should be modified." + ); + assert!(contract_stake.staked_future.is_some()); assert!( contract_stake.get(era_1, period_1).is_none(), @@ -2237,6 +2242,8 @@ fn contract_stake_amount_stake_is_ok() { "Stake is only valid from next era." ); assert_eq!(entry_1_1.total(), amount_1); + assert_eq!(entry_1_1.for_type(Subperiod::Voting), amount_1); + assert!(entry_1_1.for_type(Subperiod::BuildAndEarn).is_zero()); // 2nd scenario - stake some more to the same era but different period type, and verify state change. let period_info_1 = PeriodInfo { @@ -2248,6 +2255,13 @@ fn contract_stake_amount_stake_is_ok() { let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); assert_eq!(entry_1_2.era, stake_era_1); assert_eq!(entry_1_2.total(), amount_1 * 2); + assert_eq!(entry_1_2.for_type(Subperiod::Voting), amount_1); + assert_eq!(entry_1_2.for_type(Subperiod::BuildAndEarn), amount_1); + assert!( + contract_stake.staked.is_empty(), + "Only future entry should be modified." + ); + assert!(contract_stake.staked_future.is_some()); // 3rd scenario - stake more to the next era, while still in the same period. let era_2 = era_1 + 2; @@ -2264,6 +2278,11 @@ fn contract_stake_amount_stake_is_ok() { entry_2_1.total() + amount_2, "Since it's the same period, stake amount must carry over from the previous entry." ); + assert!( + !contract_stake.staked.is_empty(), + "staked should keep the old future entry" + ); + assert!(contract_stake.staked_future.is_some()); // 4th scenario - stake some more to the next era, but this time also bump the period. let era_3 = era_2 + 3; @@ -2293,6 +2312,11 @@ fn contract_stake_amount_stake_is_ok() { amount_3, "No carry over from previous entry since period has changed." ); + assert!( + contract_stake.staked.is_empty(), + "New period, all stakes should be reset so 'staked' should be empty." + ); + assert!(contract_stake.staked_future.is_some()); // 5th scenario - stake to the next era let era_4 = era_3 + 1; @@ -2305,6 +2329,11 @@ fn contract_stake_amount_stake_is_ok() { assert_eq!(entry_4_2.era, stake_era_4); assert_eq!(entry_4_2.period, period_2); assert_eq!(entry_4_2.total(), amount_3 + amount_4); + assert!( + !contract_stake.staked.is_empty(), + "staked should keep the old future entry" + ); + assert!(contract_stake.staked_future.is_some()); } #[test] @@ -2333,24 +2362,65 @@ fn contract_stake_amount_unstake_is_ok() { contract_stake.staked_amount(period, Subperiod::Voting), stake_amount - amount_1 ); + assert!(contract_stake.staked.is_empty()); + assert!(contract_stake.staked_future.is_some()); - // 2nd scenario - unstake in the future era, entries should be aligned to the current era + // 2nd scenario - unstake in the next era let period_info = PeriodInfo { number: period, subperiod: Subperiod::BuildAndEarn, subperiod_end_era: 40, }; - let era_2 = era_1 + 3; + let era_2 = era_1 + 1; + + contract_stake.unstake(amount_1, period_info, era_2); + assert_eq!( + contract_stake.total_staked_amount(period), + stake_amount - amount_1 * 2 + ); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::Voting), + stake_amount - amount_1 * 2 + ); + assert!( + !contract_stake.staked.is_empty(), + "future entry should be moved over to the current entry" + ); + assert!( + contract_stake.staked_future.is_none(), + "future entry should be cleaned up since it refers to the current era" + ); + + // 3rd scenario - bump up unstake eras by more than 1, entries should be aligned to the current era + let era_3 = era_2 + 3; let amount_2 = 7; - contract_stake.unstake(amount_2, period_info, era_2); + contract_stake.unstake(amount_2, period_info, era_3); assert_eq!( contract_stake.total_staked_amount(period), - stake_amount - amount_1 - amount_2 + stake_amount - amount_1 * 2 - amount_2 ); assert_eq!( contract_stake.staked_amount(period, Subperiod::Voting), - stake_amount - amount_1 - amount_2 + stake_amount - amount_1 * 2 - amount_2 ); + assert_eq!( + contract_stake.staked.era, era_3, + "Should be aligned to the current era." + ); + assert!( + contract_stake.staked_future.is_none(), + "future enry should remain 'None'" + ); + + // 4th scenario - do a full unstake with existing future entry, expect a cleanup + contract_stake.stake(stake_amount, period_info, era_3); + contract_stake.unstake( + contract_stake.total_staked_amount(period), + period_info, + era_3, + ); + assert!(contract_stake.staked.is_empty()); + assert!(contract_stake.staked_future.is_none()); } #[test] @@ -2391,6 +2461,10 @@ fn era_reward_span_push_and_get_works() { // Get the values and verify they are as expected assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); + + // Try and get the values outside of the span + assert!(era_reward_span.get(era_reward_span.first_era() - 1).is_none()); + assert!(era_reward_span.get(era_reward_span.last_era() + 1).is_none()); } #[test] diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 0fef9d4a99..b94a2d01cb 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -1142,7 +1142,6 @@ impl ContractStakeAmount { /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`. pub fn stake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { - // TODO: tests need to be re-writen for this after the refactoring let stake_era = current_era.saturating_add(1); match self.staked_future.as_mut() { @@ -1180,8 +1179,6 @@ impl ContractStakeAmount { /// Unstake the specified `amount` from the contract, for the specified `subperiod` and `era`. pub fn unstake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { - // TODO: tests need to be re-writen for this after the refactoring - // First align entries - we only need to keep track of the current era, and the next one match self.staked_future { // Future entry exists, but it covers current or older era. diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index c7f0f14602..d3ee064fb1 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -27,7 +27,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, ConstU64, Currency, EitherOfDiverse, + AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, }, weights::{ From 86ba902ff7cd2572b104b2a9c988a42299b91e2e Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 15 Nov 2023 11:29:56 +0100 Subject: [PATCH 63/86] Finished with type tests for now --- pallets/dapp-staking-v3/coverage.sh | 3 +- pallets/dapp-staking-v3/src/lib.rs | 4 +- .../dapp-staking-v3/src/test/testing_utils.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 171 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 42 ++++- 5 files changed, 211 insertions(+), 13 deletions(-) diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh index e0b6128b56..767875743c 100755 --- a/pallets/dapp-staking-v3/coverage.sh +++ b/pallets/dapp-staking-v3/coverage.sh @@ -2,7 +2,8 @@ targets=("protocol_state" "account_ledger" "dapp_info" "period_info" "era_info" \ "stake_amount" "singular_staking_info" "contract_stake_amount" "era_reward_span" \ - "period_end_info" "era_stake_pair_iter") + "period_end_info" "era_stake_pair_iter" "tier_threshold" "tier_params" "tier_configuration" \ + "dapp_tier_rewards" ) for target in "${targets[@]}" do diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e9ca6e7c87..78efab7fdd 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1372,7 +1372,7 @@ pub mod pallet { let (amount, tier_id) = dapp_tiers - .try_consume(dapp_info.id) + .try_claim(dapp_info.id) .map_err(|error| match error { DAppTierError::NoDAppInTiers => Error::::NoClaimableRewards, DAppTierError::RewardAlreadyClaimed => Error::::DAppRewardAlreadyClaimed, @@ -1668,7 +1668,7 @@ pub mod pallet { } // 4. - // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids). + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). // TODO & Idea: perhaps use BTreeMap instead? It will "sort" automatically based on dApp Id, and we can efficiently remove entries, // reducing PoV size step by step. // It's a trade-off between speed and PoV size. Although both are quite minor, so maybe it doesn't matter that much. diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index e2203397a3..ac590a67b2 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -920,7 +920,7 @@ pub(crate) fn assert_claim_dapp_reward( .expect("Entry must exist.") .clone(); - info.try_consume(dapp_info.id).unwrap() + info.try_claim(dapp_info.id).unwrap() }; // Claim dApp reward & verify event @@ -960,7 +960,7 @@ pub(crate) fn assert_claim_dapp_reward( .expect("Entry must exist.") .clone(); assert_eq!( - info.try_consume(dapp_info.id), + info.try_claim(dapp_info.id), Err(DAppTierError::RewardAlreadyClaimed), "It must not be possible to claim the same reward twice!.", ); diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 5244ed5476..7ee0512a71 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -2463,8 +2463,12 @@ fn era_reward_span_push_and_get_works() { assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); // Try and get the values outside of the span - assert!(era_reward_span.get(era_reward_span.first_era() - 1).is_none()); - assert!(era_reward_span.get(era_reward_span.last_era() + 1).is_none()); + assert!(era_reward_span + .get(era_reward_span.first_era() - 1) + .is_none()); + assert!(era_reward_span + .get(era_reward_span.last_era() + 1) + .is_none()); } #[test] @@ -2501,7 +2505,102 @@ fn era_reward_span_fails_when_expected() { } #[test] -fn tier_slot_configuration_basic_tests() { +fn tier_threshold_is_ok() { + let amount = 100; + + // Fixed TVL + let fixed_threshold = TierThreshold::FixedTvlAmount { amount }; + assert!(fixed_threshold.is_satisfied(amount)); + assert!(fixed_threshold.is_satisfied(amount + 1)); + assert!(!fixed_threshold.is_satisfied(amount - 1)); + + // Dynamic TVL + let dynamic_threshold = TierThreshold::DynamicTvlAmount { + amount, + minimum_amount: amount / 2, // not important + }; + assert!(dynamic_threshold.is_satisfied(amount)); + assert!(dynamic_threshold.is_satisfied(amount + 1)); + assert!(!dynamic_threshold.is_satisfied(amount - 1)); +} + +#[test] +fn tier_params_check_is_ok() { + // Prepare valid params + get_u32_type!(TiersNum, 3); + let params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(60), + Permill::from_percent(30), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(70), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 1000, + minimum_amount: 100, + }, + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 10, + }, + TierThreshold::FixedTvlAmount { amount: 10 }, + ]) + .unwrap(), + }; + assert!(params.is_valid()); + + // 1st scenario - sums are below 100%, and that is ok + let mut new_params = params.clone(); + new_params.reward_portion = BoundedVec::try_from(vec![ + Permill::from_percent(59), + Permill::from_percent(30), + Permill::from_percent(10), + ]) + .unwrap(); + new_params.slot_distribution = BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(19), + Permill::from_percent(70), + ]) + .unwrap(); + assert!(params.is_valid()); + + // 2nd scenario - reward portion is too much + let mut new_params = params.clone(); + new_params.reward_portion = BoundedVec::try_from(vec![ + Permill::from_percent(61), + Permill::from_percent(30), + Permill::from_percent(10), + ]) + .unwrap(); + assert!(!new_params.is_valid()); + + // 3rd scenario - tier distribution is too much + let mut new_params = params.clone(); + new_params.slot_distribution = BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(71), + ]) + .unwrap(); + assert!(!new_params.is_valid()); + + // 4th scenario - incorrect vector length + let mut new_params = params.clone(); + new_params.tier_thresholds = + BoundedVec::try_from(vec![TierThreshold::FixedTvlAmount { amount: 10 }]).unwrap(); + assert!(!new_params.is_valid()); +} + +#[test] +fn tier_configuration_basic_tests() { // TODO: this should be expanded & improved later get_u32_type!(TiersNum, 4); let params = TierParameters:: { @@ -2554,3 +2653,69 @@ fn tier_slot_configuration_basic_tests() { // TODO: expand tests, add more sanity checks (e.g. tier 3 requirement should never be lower than tier 4, etc.) } + +#[test] +fn dapp_tier_rewards_basic_tests() { + get_u32_type!(NumberOfDApps, 8); + get_u32_type!(NumberOfTiers, 3); + + // Example dApps & rewards + let dapps = vec![ + DAppTier { + dapp_id: 1, + tier_id: Some(0), + }, + DAppTier { + dapp_id: 2, + tier_id: Some(0), + }, + DAppTier { + dapp_id: 3, + tier_id: Some(1), + }, + DAppTier { + dapp_id: 5, + tier_id: Some(1), + }, + DAppTier { + dapp_id: 6, + tier_id: Some(2), + }, + ]; + let tier_rewards = vec![300, 20, 1]; + let period = 2; + + let mut dapp_tier_rewards = DAppTierRewards::::new( + dapps.clone(), + tier_rewards.clone(), + period, + ) + .expect("Bounds are respected."); + + // 1st scenario - claim reward for a dApps + let tier_id = dapps[0].tier_id.unwrap(); + assert_eq!( + dapp_tier_rewards.try_claim(dapps[0].dapp_id), + Ok((tier_rewards[tier_id as usize], tier_id)) + ); + + let tier_id = dapps[3].tier_id.unwrap(); + assert_eq!( + dapp_tier_rewards.try_claim(dapps[3].dapp_id), + Ok((tier_rewards[tier_id as usize], tier_id)) + ); + + // 2nd scenario - try to claim already claimed reward + assert_eq!( + dapp_tier_rewards.try_claim(dapps[0].dapp_id), + Err(DAppTierError::RewardAlreadyClaimed), + "Cannot claim the same reward twice." + ); + + // 3rd scenario - claim for a dApp that is not in the list + assert_eq!( + dapp_tier_rewards.try_claim(4), + Err(DAppTierError::NoDAppInTiers), + "dApp doesn't exist in the list so no rewards can be claimed." + ); +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index b94a2d01cb..47d6565879 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -70,7 +70,7 @@ use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, + traits::{AtLeast32BitUnsigned, CheckedAdd, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; pub use sp_std::{fmt::Debug, vec::Vec}; @@ -1356,6 +1356,9 @@ impl TierThreshold { Self::DynamicTvlAmount { amount, .. } => stake >= *amount, } } + + // TODO: maybe add a check that compares `Self` to another threshold and ensures it has lower requirements? + // Could be useful to have this check as a sanity check when params are configured. } /// Top level description of tier slot parameters used to calculate tier configuration. @@ -1373,11 +1376,13 @@ impl TierThreshold { pub struct TierParameters> { /// Reward distribution per tier, in percentage. /// First entry refers to the first tier, and so on. - /// The sum of all values must be exactly equal to 1. + /// The sum of all values must not exceed 100%. + /// In case it is less, portion of rewards will never be distributed. pub reward_portion: BoundedVec, /// Distribution of number of slots per tier, in percentage. /// First entry refers to the first tier, and so on. - /// The sum of all values must be exactly equal to 1. + /// The sum of all values must not exceed 100%. + /// In case it is less, slot capacity will never be fully filled. pub slot_distribution: BoundedVec, /// Requirements for entry into each tier. /// First entry refers to the first tier, and so on. @@ -1388,11 +1393,36 @@ impl> TierParameters { /// Check if configuration is valid. /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`. pub fn is_valid(&self) -> bool { + // Reward portions sum should not exceed 100%. + if self + .reward_portion + .iter() + .fold(Some(Permill::zero()), |acc, permill| match acc { + Some(acc) => acc.checked_add(permill), + None => None, + }) + .is_none() + { + return false; + } + + // Slot distribution sum should not exceed 100%. + if self + .slot_distribution + .iter() + .fold(Some(Permill::zero()), |acc, permill| match acc { + Some(acc) => acc.checked_add(permill), + None => None, + }) + .is_none() + { + return false; + } + let number_of_tiers: usize = NT::get() as usize; number_of_tiers == self.reward_portion.len() && number_of_tiers == self.slot_distribution.len() && number_of_tiers == self.tier_thresholds.len() - // TODO: Make check more detailed, verify that entries sum up to 1 or 100% } } @@ -1611,6 +1641,8 @@ impl, NT: Get> DAppTierRewards { rewards: Vec, period: PeriodNumber, ) -> Result { + // TODO: should this part of the code ensure that dapps are sorted by Id? + let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; Ok(Self { @@ -1622,7 +1654,7 @@ impl, NT: Get> DAppTierRewards { /// Consume reward for the specified dapp id, returning its amount and tier Id. /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`. - pub fn try_consume(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { + pub fn try_claim(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { // Check if dApp Id exists. let dapp_idx = self .dapps From 94998aa8b6bc6a6e73e5395daf5ae74d8bac9ec0 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 15 Nov 2023 14:38:10 +0100 Subject: [PATCH 64/86] Log, remove some TODOs --- Cargo.lock | 1 + pallets/dapp-staking-v3/Cargo.toml | 2 ++ pallets/dapp-staking-v3/src/lib.rs | 33 +++++++++++++++++++----------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c235a3ebf9..d7a797c1f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7116,6 +7116,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "num-traits", "pallet-balances", "parity-scale-codec", diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index 0de398ce18..e01f39f55c 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -12,6 +12,7 @@ frame-support = { workspace = true } frame-system = { workspace = true } num-traits = { workspace = true } parity-scale-codec = { workspace = true } +log = { workspace = true } scale-info = { workspace = true } serde = { workspace = true, optional = true } @@ -32,6 +33,7 @@ pallet-balances = { workspace = true } default = ["std"] std = [ "serde", + "log/std", "parity-scale-codec/std", "scale-info/std", "num-traits/std", diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 78efab7fdd..5c0524f6ea 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -71,6 +71,8 @@ mod dsv3_weight; // Lock identifier for the dApp staking pallet const STAKING_ID: LockIdentifier = *b"dapstake"; +const LOG: &str = "dapp-staking"; + // TODO: add tracing! #[frame_support::pallet] @@ -96,6 +98,8 @@ pub mod pallet { /// Currency used for staking. /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) + // https://github.com/paritytech/substrate/pull/12951/ + // Look at nomination pools implementation for reference! type Currency: LockableCurrency< Self::AccountId, Moment = Self::BlockNumber, @@ -342,7 +346,6 @@ pub mod pallet { #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; - // TODO: where to track TierLabels? E.g. a label to bootstrap a dApp into a specific tier. /// Map of all dApps integrated into dApp staking protocol. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< @@ -620,8 +623,14 @@ pub mod pallet { let era_span_index = Self::era_reward_span_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); - // TODO: Error "cannot" happen here. Log an error if it does though. - let _ = span.push(current_era, era_reward); + if let Err(_) = span.push(current_era, era_reward) { + // This must never happen but we log the error just in case. + log::error!( + target: LOG, + "Failed to push era {} into the era reward span.", + current_era + ); + } EraRewards::::insert(&era_span_index, span); Self::deposit_event(Event::::NewEra { era: next_era }); @@ -927,9 +936,6 @@ pub mod pallet { 0 }; - // TODO: discussion point - this will "kill" users ability to withdraw past rewards. - // This can be handled by the frontend though. - Self::update_ledger(&account, ledger); CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_removed(amount); @@ -1000,7 +1006,13 @@ pub mod pallet { let mut ledger = Ledger::::get(&account); - // TODO: suggestion is to change this a bit so we clean up ledger if rewards have expired + // In case old stake rewards are unclaimed & have expired, clean them up. + let threshold_period = Self::oldest_claimable_period(current_period); + if ledger.maybe_cleanup_expired(threshold_period) { + Self::update_ledger(&account, ledger); + } + // TODO: add a test for this! + // 1. // Increase stake amount for the next era & current period in staker's ledger ledger @@ -1014,8 +1026,6 @@ pub mod pallet { _ => Error::::InternalStakeError, })?; - // TODO: also change this to check if rewards have expired - // 2. // Update `StakerInfo` storage with the new stake amount on the specified contract. // @@ -1463,7 +1473,6 @@ pub mod pallet { Ok(()) } - // TODO: an alternative to this could would be to allow `unstake` call to cleanup old entries, however that means more complexity in that call /// Used to unstake funds from a contract that was unregistered after an account staked on it. #[pallet::call_index(15)] #[pallet::weight(Weight::zero())] @@ -1612,13 +1621,13 @@ pub mod pallet { period: PeriodNumber, dapp_reward_pool: Balance, ) -> DAppTierRewardsFor { - let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); + let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts as usize); // 1. // Iterate over all staked dApps. // This is bounded by max amount of dApps we allow to be registered. for (dapp_id, stake_amount) in ContractStake::::iter() { - // Skip dApps which don't have ANY amount staked (TODO: potential improvement is to prune all dApps below minimum threshold) + // Skip dApps which don't have ANY amount staked let stake_amount = match stake_amount.get(era, period) { Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, _ => continue, From 06551969057f3e18263100cc2a81727f5a333d4c Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 15 Nov 2023 15:50:06 +0100 Subject: [PATCH 65/86] Changes --- pallets/dapp-staking-v3/coverage.sh | 6 +++++- pallets/dapp-staking-v3/src/lib.rs | 24 ++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh index 767875743c..a74d0ec2b4 100755 --- a/pallets/dapp-staking-v3/coverage.sh +++ b/pallets/dapp-staking-v3/coverage.sh @@ -8,4 +8,8 @@ targets=("protocol_state" "account_ledger" "dapp_info" "period_info" "era_info" for target in "${targets[@]}" do cargo tarpaulin -p pallet-dapp-staking-v3 -o=html --output-dir=./coverage/$target -- $target -done \ No newline at end of file +done + +# Also need to check the coverage when only running extrinsic tests (disable type tests) + +# Also need similar approach to extrinsic testing, as above \ No newline at end of file diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 5c0524f6ea..5d2be9d5f3 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -500,8 +500,6 @@ pub mod pallet { fn on_initialize(now: BlockNumberFor) -> Weight { let mut protocol_state = ActiveProtocolState::::get(); - // TODO: maybe do lazy history cleanup in this function? - // We should not modify pallet storage while in maintenance mode. // This is a safety measure, since maintenance mode is expected to be // enabled in case some misbehavior or corrupted storage is detected. @@ -615,7 +613,6 @@ pub mod pallet { }; // Update storage items - protocol_state.era = next_era; ActiveProtocolState::::put(protocol_state); @@ -633,6 +630,19 @@ pub mod pallet { } EraRewards::::insert(&era_span_index, span); + // TODO: maybe do lazy history cleanup in this function? + // if let Some(expired_period) = Self::oldest_claimable_period().checked_sub(1) { + // if let Some(expired_period_info) = PeriodEnd::::get(&expired_period) { + // let final_era = expired_period_info.final_era; + // let expired_era_span_index = Self::era_reward_span_index(final_era); + + // let era_reward_span = EraRewards::::get(&expired_era_span_index) + // .unwrap_or_default(); + // if era_reward_span.last_era() + + // } + // } + Self::deposit_event(Event::::NewEra { era: next_era }); if let Some(period_event) = maybe_period_event { Self::deposit_event(period_event); @@ -1007,10 +1017,8 @@ pub mod pallet { let mut ledger = Ledger::::get(&account); // In case old stake rewards are unclaimed & have expired, clean them up. - let threshold_period = Self::oldest_claimable_period(current_period); - if ledger.maybe_cleanup_expired(threshold_period) { - Self::update_ledger(&account, ledger); - } + let threshold_period = Self::oldest_claimable_period(protocol_state.period_number()); + let _ignore = ledger.maybe_cleanup_expired(threshold_period); // TODO: add a test for this! // 1. @@ -1621,7 +1629,7 @@ pub mod pallet { period: PeriodNumber, dapp_reward_pool: Balance, ) -> DAppTierRewardsFor { - let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts as usize); + let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts::get() as usize); // 1. // Iterate over all staked dApps. From b9832da046b261020d4bd80cc1ce7045822e3d01 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 16 Nov 2023 09:37:16 +0100 Subject: [PATCH 66/86] Bug fix for unstake era, more tests --- pallets/dapp-staking-v3/Cargo.toml | 2 +- pallets/dapp-staking-v3/src/lib.rs | 24 +-- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- pallets/dapp-staking-v3/src/test/tests.rs | 155 ++++++++++++++---- .../dapp-staking-v3/src/test/tests_types.rs | 10 +- pallets/dapp-staking-v3/src/types.rs | 65 ++++---- 6 files changed, 178 insertions(+), 80 deletions(-) diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index e01f39f55c..d38a23eaf1 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -10,9 +10,9 @@ repository.workspace = true [dependencies] frame-support = { workspace = true } frame-system = { workspace = true } +log = { workspace = true } num-traits = { workspace = true } parity-scale-codec = { workspace = true } -log = { workspace = true } scale-info = { workspace = true } serde = { workspace = true, optional = true } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 5d2be9d5f3..fd66cfa549 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -296,8 +296,8 @@ pub mod pallet { NoUnlockingChunks, /// The amount being staked is too large compared to what's available for staking. UnavailableStakeFunds, - /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. - UnclaimedRewardsFromPastPeriods, + /// There are unclaimed rewards remaining from past eras or periods. They should be claimed before attempting any stake modification again. + UnclaimedRewards, /// An unexpected error occured while trying to stake. InternalStakeError, /// Total staked amount on contract is below the minimum required value. @@ -1019,7 +1019,6 @@ pub mod pallet { // In case old stake rewards are unclaimed & have expired, clean them up. let threshold_period = Self::oldest_claimable_period(protocol_state.period_number()); let _ignore = ledger.maybe_cleanup_expired(threshold_period); - // TODO: add a test for this! // 1. // Increase stake amount for the next era & current period in staker's ledger @@ -1027,7 +1026,7 @@ pub mod pallet { .add_stake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards } AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, // Defensive check, should never happen @@ -1052,12 +1051,15 @@ pub mod pallet { { (staking_info, false) } - // Entry exists but period doesn't match. Either reward should be claimed or cleaned up. - Some(_) => { - return Err(Error::::UnclaimedRewardsFromPastPeriods.into()); + // Entry exists but period doesn't match. Bonus reward might still be claimable. + Some(staking_info) + if staking_info.period_number() >= threshold_period + && staking_info.is_loyal() => + { + return Err(Error::::UnclaimedRewards.into()); } - // No entry exists - None => ( + // No valid entry exists + _ => ( SingularStakingInfo::new( protocol_state.period_number(), protocol_state.subperiod(), @@ -1168,7 +1170,7 @@ pub mod pallet { .map_err(|err| match err { // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards } AccountLedgerError::UnstakeAmountLargerThanStake => { Error::::UnstakeAmountTooLarge @@ -1457,7 +1459,7 @@ pub mod pallet { .map_err(|err| match err { // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards } _ => Error::::InternalUnstakeError, })?; diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index c97a563f1f..0100959e81 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -304,7 +304,7 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_advance_to_next_subperiod() { +pub(crate) fn advance_to_next_subperiod() { let subperiod = ActiveProtocolState::::get().subperiod(); while ActiveProtocolState::::get().subperiod() == subperiod { run_for_blocks(1); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 9fa4844083..f0ddc4eb81 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -26,9 +26,6 @@ use crate::{ use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; use sp_runtime::traits::Zero; -// TODO: test scenarios -// 1. user is staking, period passes, they can unlock their funds which were previously staked - #[test] fn print_test() { ExtBuilder::build().execute_with(|| { @@ -834,10 +831,44 @@ fn stake_basic_example_is_ok() { let lock_amount = 300; assert_lock(account, lock_amount); - // Stake some amount, and then some more - let (stake_amount_1, stake_amount_2) = (31, 29); + // Stake some amount, and then some more in the same era. + let (stake_1, stake_2) = (31, 29); + assert_stake(account, &smart_contract, stake_1); + assert_stake(account, &smart_contract, stake_2); + }) +} + +#[test] +fn stake_after_expiry_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + // Lock & stake some amount + let account = 2; + let lock_amount = 300; + let (stake_amount_1, stake_amount_2) = (200, 100); + assert_lock(account, lock_amount); assert_stake(account, &smart_contract, stake_amount_1); + + // Advance so far that the stake rewards expire. + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods + 1, + ); + + // Sanity check that the rewards have expired + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::RewardExpired, + ); + + // Calling stake again should work, expired stake entries should be cleaned up assert_stake(account, &smart_contract, stake_amount_2); + assert_stake(account, &smart_contract, stake_amount_1); }) } @@ -904,7 +935,7 @@ fn stake_in_final_era_fails() { } #[test] -fn stake_fails_if_unclaimed_rewards_from_past_period_remain() { +fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let smart_contract = MockSmartContract::default(); @@ -917,7 +948,7 @@ fn stake_fails_if_unclaimed_rewards_from_past_period_remain() { advance_to_next_period(); assert_noop!( DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards ); }) } @@ -992,8 +1023,6 @@ fn stake_fails_due_to_too_small_staking_amount() { }) } -// TODO: add tests to cover staking & unstaking with unclaimed rewards! - #[test] fn unstake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { @@ -1013,8 +1042,6 @@ fn unstake_basic_example_is_ok() { // Unstake some amount, in the current era. let unstake_amount_1 = 3; assert_unstake(account, &smart_contract, unstake_amount_1); - - // TODO: scenario where we unstake AFTER advancing an era and claiming rewards }) } @@ -1140,33 +1167,26 @@ fn unstake_from_non_staked_contract_fails() { } #[test] -fn unstake_from_a_contract_staked_in_past_period_fails() { +fn unstake_with_unclaimed_rewards_fails() { ExtBuilder::build().execute_with(|| { - // Register smart contract & lock some amount - let smart_contract_1 = MockSmartContract::Wasm(1); - let smart_contract_2 = MockSmartContract::Wasm(2); - assert_register(1, &smart_contract_1); - assert_register(1, &smart_contract_2); + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::Wasm(1); + assert_register(1, &smart_contract); let account = 2; assert_lock(account, 300); - - // Stake some amount on the 2nd contract. let stake_amount = 100; - assert_stake(account, &smart_contract_2, stake_amount); + assert_stake(account, &smart_contract, stake_amount); - // Advance to the next period, and stake on the 1st contract. - advance_to_next_period(); - // TODO: need to implement reward claiming for this check to work! - // assert_stake(account, &smart_contract_1, stake_amount); - // Try to unstake from the 2nd contract, which is no longer staked on due to period change. - // assert_noop!( - // DappStaking::unstake( - // RuntimeOrigin::signed(account), - // smart_contract_2, - // 1, - // ), - // Error::::UnstakeFromPastPeriod - // ); + // Advance 1 era, try to unstake and it should work since we're modifying the current era stake. + advance_to_next_era(); + assert_unstake(account, &smart_contract, 1); + + // Advance 1 more era, creating claimable rewards. Unstake should fail now. + advance_to_next_era(); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 1), + Error::::UnclaimedRewards + ); }) } @@ -1314,7 +1334,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_period( ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); - advance_to_advance_to_next_subperiod(); + advance_to_next_subperiod(); advance_to_era( ActiveProtocolState::::get() .period_info @@ -1444,7 +1464,7 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { assert_lock(account, lock_amount); // Stake in Build&Earn period type, advance to next era and try to claim bonus reward - advance_to_advance_to_next_subperiod(); + advance_to_next_subperiod(); assert_eq!( ActiveProtocolState::::get().subperiod(), Subperiod::BuildAndEarn, @@ -1637,3 +1657,68 @@ fn claim_dapp_reward_twice_for_same_era_fails() { assert_claim_dapp_reward(account, &smart_contract, claim_era_2); }) } + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +/////// More complex & composite scenarios, maybe move them into a separate file + +#[test] +fn unlock_after_staked_period_ends_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 101; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance to the next period, and ensure stake is reset and can be fully unlocked + advance_to_next_period(); + assert!(Ledger::::get(&account) + .staked_amount(ActiveProtocolState::::get().period_number()) + .is_zero()); + assert_unlock(account, amount); + assert_eq!(Ledger::::get(&account).unlocking_amount(), amount); + }) +} + +#[test] +fn unstake_from_a_contract_staked_in_past_period_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + let account = 2; + assert_lock(account, 300); + + // Stake some amount on the 2nd contract. + let stake_amount = 100; + assert_stake(account, &smart_contract_2, stake_amount); + + // Advance to the next period, and stake on the 1st contract. + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // Try to unstake from the 2nd contract, which is no longer staked on due to period change. + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract_2, 1,), + Error::::UnstakeFromPastPeriod + ); + + // Staking on the 1st contract should succeed since we haven't staked on it before so there are no bonus rewards to claim + assert_stake(account, &smart_contract_1, stake_amount); + + // Even with active stake on the 1st contract, unstake from 2nd should still fail since period change reset its stake. + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract_2, 1,), + Error::::UnstakeFromPastPeriod + ); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 7ee0512a71..6829a96b2a 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -902,9 +902,15 @@ fn account_ledger_unstake_from_invalid_era_fails() { .add_stake_amount(amount_1, era_1, period_info_1) .is_ok()); - // Try to unstake from the next era, it should fail. + // Try to unstake from the current & next era, it should work. + assert!(acc_ledger.unstake_amount(1, era_1, period_info_1).is_ok()); + assert!(acc_ledger + .unstake_amount(1, era_1 + 1, period_info_1) + .is_ok()); + + // Try to unstake from the stake era + 2, it should fail since it would mean we have unclaimed rewards. assert_eq!( - acc_ledger.unstake_amount(1, era_1 + 1, period_info_1), + acc_ledger.unstake_amount(1, era_1 + 2, period_info_1), Err(AccountLedgerError::InvalidEra) ); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 47d6565879..539116b7b0 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -518,34 +518,6 @@ where } } - /// Verify that current era and period info arguments are valid for `stake` and `unstake` operations. - fn verify_stake_unstake_args( - &self, - era: EraNumber, - current_period_info: &PeriodInfo, - ) -> Result<(), AccountLedgerError> { - if !self.staked.is_empty() { - // In case entry for the current era exists, it must match the era exactly. - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - // In case it doesn't (i.e. first time staking), then the future era must match exactly - // one era after the one provided via argument. - } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era.saturating_add(1) { - return Err(AccountLedgerError::InvalidEra); - } - if stake_amount.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - } - - Ok(()) - } - /// Adds the specified amount to total staked amount, if possible. /// /// Staking can only be done for the ongoing period, and era. @@ -567,7 +539,24 @@ where return Ok(()); } - self.verify_stake_unstake_args(era, ¤t_period_info)?; + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era.saturating_add(1) { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + } if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); @@ -605,7 +594,23 @@ where return Ok(()); } - self.verify_stake_unstake_args(era, ¤t_period_info)?; + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must either be the current or the next era. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era.saturating_add(1) && stake_amount.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + } // User must be precise with their unstake amount. if self.staked_amount(current_period_info.number) < amount { From 2d78cb0c4b96a8585aaea5467a67c2796dceceb6 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 16 Nov 2023 09:53:52 +0100 Subject: [PATCH 67/86] Formatting, extra test --- pallets/dapp-staking-v3/src/lib.rs | 4 ++- pallets/dapp-staking-v3/src/test/tests.rs | 33 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index aae533f8d3..fd66cfa549 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1514,7 +1514,9 @@ pub mod pallet { // Remove expired ledger stake entries, if needed. let threshold_period = Self::oldest_claimable_period(current_period); let mut ledger = Ledger::::get(&account); - ledger.contract_stake_count.saturating_reduce(entries_to_delete as u32); + ledger + .contract_stake_count + .saturating_reduce(entries_to_delete as u32); if ledger.maybe_cleanup_expired(threshold_period) { Self::update_ledger(&account, ledger); } diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index f0ddc4eb81..405a51e40c 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1722,3 +1722,36 @@ fn unstake_from_a_contract_staked_in_past_period_fails() { ); }) } + +#[test] +fn stake_and_unstake_after_reward_claim_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let amount = 400; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount - 100); + + // Advance 2 eras so we have claimable rewards. Both stake & unstake should fail. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 1), + Error::::UnclaimedRewards + ); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 1), + Error::::UnclaimedRewards + ); + + // Claim rewards, unstake should work now. + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_stake(account, &smart_contract, 1); + assert_unstake(account, &smart_contract, 1); + }) +} From 05e43c817560dfaa7d87e46ff32ac3ad2f870a9f Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 16 Nov 2023 12:48:02 +0100 Subject: [PATCH 68/86] Unregistered unstake, expired cleanup, test utils & tests --- pallets/dapp-staking-v3/src/lib.rs | 53 ++++-- .../dapp-staking-v3/src/test/testing_utils.rs | 160 +++++++++++++++++- pallets/dapp-staking-v3/src/test/tests.rs | 75 ++++++++ 3 files changed, 269 insertions(+), 19 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index fd66cfa549..be5ceede9c 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -47,7 +47,7 @@ use frame_support::{ }; use frame_system::pallet_prelude::*; use sp_runtime::{ - traits::{BadOrigin, One, Saturating, Zero}, + traits::{BadOrigin, One, Saturating, UniqueSaturatedInto, Zero}, Perbill, Permill, }; pub use sp_std::vec::Vec; @@ -180,7 +180,9 @@ pub mod pallet { #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event { /// New era has started. - NewEra { era: EraNumber }, + NewEra { + era: EraNumber, + }, /// New period has started. NewPeriod { subperiod: Subperiod, @@ -263,6 +265,10 @@ pub mod pallet { smart_contract: T::SmartContract, amount: Balance, }, + ExpiredEntriesRemoved { + account: T::AccountId, + count: u16, + }, } #[pallet::error] @@ -335,6 +341,8 @@ pub mod pallet { ContractStillActive, /// There are too many contract stake entries for the account. This can be cleaned up by either unstaking or cleaning expired entries. TooManyStakedContracts, + /// There are no expired entries to cleanup for the account. + NoExpiredEntriesToCleanup, } /// General information about dApp staking protocol state. @@ -1425,7 +1433,6 @@ pub mod pallet { origin: OriginFor, smart_contract: T::SmartContract, ) -> DispatchResult { - // TODO: tests are missing but will be added later. Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1457,7 +1464,7 @@ pub mod pallet { ledger .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { - // These are all defensive checks, which should never happen since we already checked them above. + // These are all defensive checks, which should never fail since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewards } @@ -1466,10 +1473,14 @@ pub mod pallet { // Update total staked amount for the next era. // This means 'fake' stake total amount has been kept until now, even though contract was unregistered. + // Although strange, it's been requested to keep it like this from the team. CurrentEraInfo::::mutate(|era_info| { era_info.unstake_amount(amount, protocol_state.subperiod()); }); + // TODO: HOWEVER, we should not pay out bonus rewards for such contracts. + // Seems wrong because it serves as discentive for unstaking & moving over to a new contract. + // Update remaining storage entries Self::update_ledger(&account, ledger); StakerInfo::::remove(&account, &smart_contract); @@ -1483,21 +1494,28 @@ pub mod pallet { Ok(()) } - /// Used to unstake funds from a contract that was unregistered after an account staked on it. + /// Cleanup expired stake entries for the contract. + /// + /// Entry is considered to be expired if: + /// 1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward. + /// 2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not. #[pallet::call_index(15)] #[pallet::weight(Weight::zero())] pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResult { - // TODO: tests are missing but will be added later. Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; let protocol_state = ActiveProtocolState::::get(); let current_period = protocol_state.period_number(); + let threshold_period = Self::oldest_claimable_period(current_period); - // Find all entries which have expired. This is bounded by max allowed number of entries. + // Find all entries which are from past periods & don't have claimable bonus rewards. + // This is bounded by max allowed number of stake entries per account. let to_be_deleted: Vec = StakerInfo::::iter_prefix(&account) .filter_map(|(smart_contract, stake_info)| { - if stake_info.period_number() < current_period { + if stake_info.period_number() < current_period && !stake_info.is_loyal() + || stake_info.period_number() < threshold_period + { Some(smart_contract) } else { None @@ -1505,21 +1523,28 @@ pub mod pallet { }) .collect(); let entries_to_delete = to_be_deleted.len(); + ensure!( + !entries_to_delete.is_zero(), + Error::::NoExpiredEntriesToCleanup + ); // Remove all expired entries. for smart_contract in to_be_deleted { StakerInfo::::remove(&account, &smart_contract); } - // Remove expired ledger stake entries, if needed. - let threshold_period = Self::oldest_claimable_period(current_period); + // Remove expired stake entries from the ledger. let mut ledger = Ledger::::get(&account); ledger .contract_stake_count - .saturating_reduce(entries_to_delete as u32); - if ledger.maybe_cleanup_expired(threshold_period) { - Self::update_ledger(&account, ledger); - } + .saturating_reduce(entries_to_delete.unique_saturated_into()); + ledger.maybe_cleanup_expired(threshold_period); // Not necessary but we do it for the sake of consistency + Self::update_ledger(&account, ledger); + + Self::deposit_event(Event::::ExpiredEntriesRemoved { + account, + count: entries_to_delete.unique_saturated_into(), + }); Ok(()) } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index ac590a67b2..55146823f6 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -414,7 +414,6 @@ pub(crate) fn assert_stake( smart_contract: &MockSmartContract, amount: Balance, ) { - // TODO: this is a huge function - I could break it down, but I'm not sure it will help with readability. let pre_snapshot = MemorySnapshot::new(); let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); let pre_staker_info = pre_snapshot @@ -473,7 +472,6 @@ pub(crate) fn assert_stake( pre_ledger.stakeable_amount(stake_period) - amount, "Stakeable amount must decrease by the 'amount'" ); - // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -575,7 +573,6 @@ pub(crate) fn assert_unstake( .expect("Entry must exist since 'unstake' is being called."); let pre_era_info = pre_snapshot.current_era_info; - let _unstake_era = pre_snapshot.active_protocol_state.era; let unstake_period = pre_snapshot.active_protocol_state.period_number(); let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod(); @@ -608,7 +605,7 @@ pub(crate) fn assert_unstake( let post_contract_stake = post_snapshot .contract_stake .get(&pre_snapshot.integrated_dapps[&smart_contract].id) - .expect("Entry must exist since 'stake' operation was successfull."); + .expect("Entry must exist since 'unstake' operation was successfull."); let post_era_info = post_snapshot.current_era_info; // 1. verify ledger @@ -624,7 +621,6 @@ pub(crate) fn assert_unstake( pre_ledger.stakeable_amount(unstake_period) + amount, "Stakeable amount must increase by the 'amount'" ); - // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -700,6 +696,7 @@ pub(crate) fn assert_unstake( "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." ); + // Check for unstake underflow. if unstake_subperiod == Subperiod::BuildAndEarn && pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn) < amount { @@ -966,6 +963,159 @@ pub(crate) fn assert_claim_dapp_reward( ); } +/// Unstake some funds from the specified unregistered smart contract. +pub(crate) fn assert_unstake_from_unregistered( + account: AccountId, + smart_contract: &MockSmartContract, +) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_staker_info = pre_snapshot + .staker_info + .get(&(account, smart_contract.clone())) + .expect("Entry must exist since 'unstake_from_unregistered' is being called."); + let pre_era_info = pre_snapshot.current_era_info; + + let amount = pre_staker_info.total_staked_amount(); + + // Unstake from smart contract & verify event + assert_ok!(DappStaking::unstake_from_unregistered( + RuntimeOrigin::signed(account), + smart_contract.clone(), + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::UnstakeFromUnregistered { + account, + smart_contract: smart_contract.clone(), + amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + let post_era_info = post_snapshot.current_era_info; + let period = pre_snapshot.active_protocol_state.period_number(); + let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod(); + + // 1. verify ledger + // ===================== + // ===================== + assert_eq!( + post_ledger.staked_amount(period), + pre_ledger.staked_amount(period) - amount, + "Stake amount must decrease by the 'amount'" + ); + assert_eq!( + post_ledger.stakeable_amount(period), + pre_ledger.stakeable_amount(period) + amount, + "Stakeable amount must increase by the 'amount'" + ); + + // 2. verify staker info + // ===================== + // ===================== + assert!( + !StakerInfo::::contains_key(&account, smart_contract), + "Entry must be deleted since contract is unregistered." + ); + + // 3. verify era info + // ========================= + // ========================= + // It's possible next era has staked more than the current era. This is because 'stake' will always stake for the NEXT era. + if pre_era_info.total_staked_amount() < amount { + assert!(post_era_info.total_staked_amount().is_zero()); + } else { + assert_eq!( + post_era_info.total_staked_amount(), + pre_era_info.total_staked_amount() - amount, + "Total staked amount for the current era must decrease by 'amount'." + ); + } + assert_eq!( + post_era_info.total_staked_amount_next_era(), + pre_era_info.total_staked_amount_next_era() - amount, + "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." + ); + + // Check for unstake underflow. + if unstake_subperiod == Subperiod::BuildAndEarn + && pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn) < amount + { + let overflow = amount - pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn); + + assert!(post_era_info + .staked_amount_next_era(Subperiod::BuildAndEarn) + .is_zero()); + assert_eq!( + post_era_info.staked_amount_next_era(Subperiod::Voting), + pre_era_info.staked_amount_next_era(Subperiod::Voting) - overflow + ); + } else { + assert_eq!( + post_era_info.staked_amount_next_era(unstake_subperiod), + pre_era_info.staked_amount_next_era(unstake_subperiod) - amount + ); + } +} + +/// Cleanup expired DB entries for the account and verify post state. +pub(crate) fn assert_cleanup_expired_entries(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + + let current_period = pre_snapshot.active_protocol_state.period_number(); + let threshold_period = DappStaking::oldest_claimable_period(current_period); + + // Find entries which should be kept, and which should be deleted + let mut to_be_deleted = Vec::new(); + let mut to_be_kept = Vec::new(); + pre_snapshot + .staker_info + .iter() + .for_each(|((inner_account, contract), entry)| { + if *inner_account == account { + if entry.period_number() < current_period && !entry.is_loyal() + || entry.period_number() < threshold_period + { + to_be_deleted.push(contract); + } else { + to_be_kept.push(contract); + } + } + }); + + // Cleanup expired entries and verify event + assert_ok!(DappStaking::cleanup_expired_entries(RuntimeOrigin::signed( + account + ))); + System::assert_last_event(RuntimeEvent::DappStaking(Event::ExpiredEntriesRemoved { + account, + count: to_be_deleted.len().try_into().unwrap(), + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + + // Ensure that correct entries have been kept + assert_eq!(post_snapshot.staker_info.len(), to_be_kept.len()); + to_be_kept.iter().for_each(|contract| { + assert!(post_snapshot + .staker_info + .contains_key(&(account, **contract))); + }); + + // Ensure that ledger has been correctly updated + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + assert!(post_ledger.staked.is_empty()); + assert!(post_ledger.staked_future.is_none()); + + let num_of_deleted_entries: u32 = to_be_deleted.len().try_into().unwrap(); + assert_eq!( + pre_ledger.contract_stake_count - num_of_deleted_entries, + post_ledger.contract_stake_count + ); +} + /// Returns from which starting era to which ending era can rewards be claimed for the specified account. /// /// If `None` is returned, there is nothing to claim. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 405a51e40c..671648c5da 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1658,6 +1658,81 @@ fn claim_dapp_reward_twice_for_same_era_fails() { }) } +#[test] +fn unstake_from_unregistered_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Unregister the smart contract, and unstake from it. + assert_unregister(&smart_contract); + assert_unstake_from_unregistered(account, &smart_contract); + }) +} + +#[test] +fn unstake_from_unregistered_fails_for_active_contract() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + assert_noop!( + DappStaking::unstake_from_unregistered(RuntimeOrigin::signed(account), smart_contract), + Error::::ContractStillActive + ); + }) +} + +#[test] +fn unstake_from_unregistered_fails_for_not_staked_contract() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + assert_unregister(&smart_contract); + + assert_noop!( + DappStaking::unstake_from_unregistered(RuntimeOrigin::signed(2), smart_contract), + Error::::NoStakingInfo + ); + }) +} + +#[test] +fn unstake_from_unregistered_fails_for_past_period() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Unregister smart contract & advance to next period + assert_unregister(&smart_contract); + advance_to_next_period(); + + assert_noop!( + DappStaking::unstake_from_unregistered(RuntimeOrigin::signed(account), smart_contract), + Error::::UnstakeFromPastPeriod + ); + }) +} + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// From cd3143d0ea267fd9f01de827a0bcc46e64d3829f Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Thu, 16 Nov 2023 15:53:33 +0100 Subject: [PATCH 69/86] Cleanup tests, minor fixes --- pallets/dapp-staking-v3/src/lib.rs | 8 +- pallets/dapp-staking-v3/src/test/mock.rs | 2 +- .../dapp-staking-v3/src/test/testing_utils.rs | 2 - pallets/dapp-staking-v3/src/test/tests.rs | 93 ++++++++++++++++++- .../dapp-staking-v3/src/test/tests_types.rs | 91 +++++++++--------- pallets/dapp-staking-v3/src/types.rs | 65 ++++++------- 6 files changed, 170 insertions(+), 91 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index be5ceede9c..2409bf0ba0 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -342,7 +342,7 @@ pub mod pallet { /// There are too many contract stake entries for the account. This can be cleaned up by either unstaking or cleaning expired entries. TooManyStakedContracts, /// There are no expired entries to cleanup for the account. - NoExpiredEntriesToCleanup, + NoExpiredEntries, } /// General information about dApp staking protocol state. @@ -1523,10 +1523,8 @@ pub mod pallet { }) .collect(); let entries_to_delete = to_be_deleted.len(); - ensure!( - !entries_to_delete.is_zero(), - Error::::NoExpiredEntriesToCleanup - ); + + ensure!(!entries_to_delete.is_zero(), Error::::NoExpiredEntries); // Remove all expired entries. for smart_contract in to_be_deleted { diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 0100959e81..d03a5f7507 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -163,7 +163,7 @@ impl pallet_dapp_staking::Config for Test { type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU32<2>; - type MaxNumberOfStakedContracts = ConstU32<3>; + type MaxNumberOfStakedContracts = ConstU32<5>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; #[cfg(feature = "runtime-benchmarks")] diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 55146823f6..371fbe343f 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -1106,8 +1106,6 @@ pub(crate) fn assert_cleanup_expired_entries(account: AccountId) { // Ensure that ledger has been correctly updated let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); let post_ledger = post_snapshot.ledger.get(&account).unwrap(); - assert!(post_ledger.staked.is_empty()); - assert!(post_ledger.staked_future.is_none()); let num_of_deleted_entries: u32 = to_be_deleted.len().try_into().unwrap(); assert_eq!( diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 671648c5da..bf1b005f8e 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -20,7 +20,7 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, - IntegratedDApps, Ledger, NextDAppId, PeriodNumber, Subperiod, + IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, }; use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; @@ -1733,6 +1733,97 @@ fn unstake_from_unregistered_fails_for_past_period() { }) } +#[test] +fn cleanup_expired_entries_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts + let contracts: Vec<_> = (1..=5).map(|id| MockSmartContract::Wasm(id)).collect(); + contracts.iter().for_each(|smart_contract| { + assert_register(1, smart_contract); + }); + let account = 2; + assert_lock(account, 1000); + + // Scenario: + // - 1st contract will be staked in the period that expires due to exceeded reward retention + // - 2nd contract will be staked in the period on the edge of expiry, with loyalty flag + // - 3rd contract will be be staked in the period on the edge of expiry, without loyalty flag + // - 4th contract will be staked in the period right before the current one, with loyalty flag + // - 5th contract will be staked in the period right before the current one, without loyalty flag + // + // Expectation: 1, 3, 5 should be removed, 2 & 4 should remain + + // 1st + assert_stake(account, &contracts[0], 13); + + // 2nd & 3rd + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_stake(account, &contracts[1], 17); + advance_to_next_subperiod(); + + assert_stake(account, &contracts[2], 19); + + // 4th & 5th + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + assert!( + reward_retention_in_periods >= 2, + "Sanity check, otherwise the test doesn't make sense." + ); + advance_to_period(reward_retention_in_periods + 1); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_stake(account, &contracts[3], 23); + advance_to_next_subperiod(); + assert_stake(account, &contracts[4], 29); + + // Finally do the test + advance_to_next_period(); + assert_cleanup_expired_entries(account); + + // Additional sanity check according to the described scenario + assert!(!StakerInfo::::contains_key(account, &contracts[0])); + assert!(!StakerInfo::::contains_key(account, &contracts[2])); + assert!(!StakerInfo::::contains_key(account, &contracts[4])); + + assert!(StakerInfo::::contains_key(account, &contracts[1])); + assert!(StakerInfo::::contains_key(account, &contracts[3])); + }) +} + +#[test] +fn cleanup_expired_entries_fails_with_no_entries() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts + let (contract_1, contract_2) = (MockSmartContract::Wasm(1), MockSmartContract::Wasm(2)); + assert_register(1, &contract_1); + assert_register(1, &contract_2); + + let account = 2; + assert_lock(account, 1000); + assert_stake(account, &contract_1, 13); + assert_stake(account, &contract_2, 17); + + // Advance only one period, rewards should still be valid. + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + assert!( + reward_retention_in_periods >= 1, + "Sanity check, otherwise the test doesn't make sense." + ); + advance_to_next_period(); + + assert_noop!( + DappStaking::cleanup_expired_entries(RuntimeOrigin::signed(account)), + Error::::NoExpiredEntries + ); + }) +} + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 6829a96b2a..b83c1d8704 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -219,31 +219,31 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { // First basic scenario // Add some lock amount, then reduce it - let first_lock_amount = 19; + let lock_amount_1 = 19; let unlock_amount = 7; - acc_ledger.add_lock_amount(first_lock_amount); + acc_ledger.add_lock_amount(lock_amount_1); acc_ledger.subtract_lock_amount(unlock_amount); assert_eq!( acc_ledger.total_locked_amount(), - first_lock_amount - unlock_amount + lock_amount_1 - unlock_amount ); assert_eq!( acc_ledger.active_locked_amount(), - first_lock_amount - unlock_amount + lock_amount_1 - unlock_amount ); assert_eq!(acc_ledger.unlocking_amount(), 0); // Second basic scenario - let first_lock_amount = first_lock_amount - unlock_amount; - let second_lock_amount = 31; - acc_ledger.add_lock_amount(second_lock_amount - first_lock_amount); - assert_eq!(acc_ledger.active_locked_amount(), second_lock_amount); + let lock_amount_1 = lock_amount_1 - unlock_amount; + let lock_amount_2 = 31; + acc_ledger.add_lock_amount(lock_amount_2 - lock_amount_1); + assert_eq!(acc_ledger.active_locked_amount(), lock_amount_2); // Subtract from the first era and verify state is as expected acc_ledger.subtract_lock_amount(unlock_amount); assert_eq!( acc_ledger.active_locked_amount(), - second_lock_amount - unlock_amount + lock_amount_2 - unlock_amount ); } @@ -432,12 +432,12 @@ fn account_ledger_stakeable_amount_works() { ); // Second scenario - some staked amount is introduced, period is still valid - let first_era = 1; + let era_1 = 1; let staked_amount = 7; acc_ledger.staked = StakeAmount { voting: 0, build_and_earn: staked_amount, - era: first_era, + era: era_1, period: period_1, }; @@ -523,7 +523,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { assert!(acc_ledger.staked_future.is_none()); // 1st scenario - stake some amount in Voting period, and ensure values are as expected. - let first_era = 1; + let era_1 = 1; let period_1 = 1; let period_info_1 = PeriodInfo { number: period_1, @@ -535,7 +535,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, period_info_1) + .add_stake_amount(stake_amount, era_1, period_info_1) .is_ok()); assert!( @@ -567,9 +567,8 @@ fn account_ledger_add_stake_amount_basic_example_works() { subperiod: Subperiod::BuildAndEarn, subperiod_end_era: 100, }; - assert!(acc_ledger - .add_stake_amount(1, first_era, period_info_2) - .is_ok()); + let era_2 = era_1 + 1; + assert!(acc_ledger.add_stake_amount(1, era_2, period_info_2).is_ok()); assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); assert_eq!( acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), @@ -588,7 +587,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { let mut acc_ledger = AccountLedger::::default(); // 1st scenario - stake some amount, and ensure values are as expected. - let first_era = 1; + let era_1 = 1; let period_1 = 1; let period_info_1 = PeriodInfo { number: period_1, @@ -603,14 +602,14 @@ fn account_ledger_add_stake_amount_advanced_example_works() { acc_ledger.staked = StakeAmount { voting: stake_amount_1, build_and_earn: 0, - era: first_era, + era: era_1, period: period_1, }; let stake_amount_2 = 2; let acc_ledger_snapshot = acc_ledger.clone(); assert!(acc_ledger - .add_stake_amount(stake_amount_2, first_era, period_info_1) + .add_stake_amount(stake_amount_2, era_1, period_info_1) .is_ok()); assert_eq!( acc_ledger.staked_amount(period_1), @@ -631,7 +630,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { .for_type(Subperiod::Voting), stake_amount_1 + stake_amount_2 ); - assert_eq!(acc_ledger.staked_future.unwrap().era, first_era + 1); + assert_eq!(acc_ledger.staked_future.unwrap().era, era_1 + 1); } #[test] @@ -640,7 +639,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let mut acc_ledger = AccountLedger::::default(); // Prep actions - let first_era = 5; + let era_1 = 5; let period_1 = 2; let period_info_1 = PeriodInfo { number: period_1, @@ -651,12 +650,12 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, period_info_1) + .add_stake_amount(stake_amount, era_1, period_info_1) .is_ok()); - // Try to add to the next era, it should fail. + // Try to add to era after next, it should fail. assert_eq!( - acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + acc_ledger.add_stake_amount(1, era_1 + 2, period_info_1), Err(AccountLedgerError::InvalidEra) ); @@ -664,7 +663,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, + era_1, PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, @@ -678,19 +677,19 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { acc_ledger.staked = StakeAmount { voting: 0, build_and_earn: stake_amount, - era: first_era, + era: era_1, period: period_1, }; acc_ledger.staked_future = None; assert_eq!( - acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, + era_1, PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, @@ -721,7 +720,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { ); // Lock some amount, and try to stake more than that - let first_era = 5; + let era_1 = 5; let period_1 = 2; let period_info_1 = PeriodInfo { number: period_1, @@ -731,16 +730,16 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); assert_eq!( - acc_ledger.add_stake_amount(lock_amount + 1, first_era, period_info_1), + acc_ledger.add_stake_amount(lock_amount + 1, era_1, period_info_1), Err(AccountLedgerError::UnavailableStakeFunds) ); // Additional check - have some active stake, and then try to overstake assert!(acc_ledger - .add_stake_amount(lock_amount - 2, first_era, period_info_1) + .add_stake_amount(lock_amount - 2, era_1, period_info_1) .is_ok()); assert_eq!( - acc_ledger.add_stake_amount(3, first_era, period_info_1), + acc_ledger.add_stake_amount(3, era_1, period_info_1), Err(AccountLedgerError::UnavailableStakeFunds) ); } @@ -1615,20 +1614,20 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { #[test] fn era_stake_pair_iter_works() { // 1st scenario - only span is given - let (first_era, last_era, amount) = (2, 5, 11); - let mut iter_1 = EraStakePairIter::new((first_era, last_era, amount), None).unwrap(); - for era in first_era..=last_era { + let (era_1, last_era, amount) = (2, 5, 11); + let mut iter_1 = EraStakePairIter::new((era_1, last_era, amount), None).unwrap(); + for era in era_1..=last_era { assert_eq!(iter_1.next(), Some((era, amount))); } assert!(iter_1.next().is_none()); // 2nd scenario - first value & span are given - let (maybe_first_era, maybe_first_amount) = (1, 7); - let maybe_first = Some((maybe_first_era, maybe_first_amount)); - let mut iter_2 = EraStakePairIter::new((first_era, last_era, amount), maybe_first).unwrap(); + let (maybe_era_1, maybe_first_amount) = (1, 7); + let maybe_first = Some((maybe_era_1, maybe_first_amount)); + let mut iter_2 = EraStakePairIter::new((era_1, last_era, amount), maybe_first).unwrap(); - assert_eq!(iter_2.next(), Some((maybe_first_era, maybe_first_amount))); - for era in first_era..=last_era { + assert_eq!(iter_2.next(), Some((maybe_era_1, maybe_first_amount))); + for era in era_1..=last_era { assert_eq!(iter_2.next(), Some((era, amount))); } } @@ -1636,19 +1635,17 @@ fn era_stake_pair_iter_works() { #[test] fn era_stake_pair_iter_returns_error_for_illegal_data() { // 1st scenario - spans are reversed; first era comes AFTER the last era - let (first_era, last_era, amount) = (2, 5, 11); - assert!(EraStakePairIter::new((last_era, first_era, amount), None).is_err()); + let (era_1, last_era, amount) = (2, 5, 11); + assert!(EraStakePairIter::new((last_era, era_1, amount), None).is_err()); // 2nd scenario - maybe_first covers the same era as the span - assert!(EraStakePairIter::new((first_era, last_era, amount), Some((first_era, 10))).is_err()); + assert!(EraStakePairIter::new((era_1, last_era, amount), Some((era_1, 10))).is_err()); // 3rd scenario - maybe_first is before the span, but not exactly 1 era before the first era in the span - assert!( - EraStakePairIter::new((first_era, last_era, amount), Some((first_era - 2, 10))).is_err() - ); + assert!(EraStakePairIter::new((era_1, last_era, amount), Some((era_1 - 2, 10))).is_err()); assert!( - EraStakePairIter::new((first_era, last_era, amount), Some((first_era - 1, 10))).is_ok(), + EraStakePairIter::new((era_1, last_era, amount), Some((era_1 - 1, 10))).is_ok(), "Sanity check." ); } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 539116b7b0..bdec006f63 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -518,6 +518,34 @@ where } } + /// Check for stake/unstake operation era & period arguments. + /// + /// Ensures that the provided era & period are valid according to the current ledger state. + fn stake_unstake_argument_check( + &self, + era: EraNumber, + current_period_info: &PeriodInfo, + ) -> Result<(), AccountLedgerError> { + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must either be the current or the next era. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era.saturating_add(1) && stake_amount.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + } + Ok(()) + } + /// Adds the specified amount to total staked amount, if possible. /// /// Staking can only be done for the ongoing period, and era. @@ -539,24 +567,7 @@ where return Ok(()); } - if !self.staked.is_empty() { - // In case entry for the current era exists, it must match the era exactly. - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - // In case it doesn't (i.e. first time staking), then the future era must match exactly - // one era after the one provided via argument. - } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era.saturating_add(1) { - return Err(AccountLedgerError::InvalidEra); - } - if stake_amount.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - } + self.stake_unstake_argument_check(era, ¤t_period_info)?; if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); @@ -594,23 +605,7 @@ where return Ok(()); } - if !self.staked.is_empty() { - // In case entry for the current era exists, it must match the era exactly. - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - // In case it doesn't (i.e. first time staking), then the future era must either be the current or the next era. - } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era.saturating_add(1) && stake_amount.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if stake_amount.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); - } - } + self.stake_unstake_argument_check(era, ¤t_period_info)?; // User must be precise with their unstake amount. if self.staked_amount(current_period_info.number) < amount { From a0efeedae20dc7be7a238ecb797fefdc7b739d55 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 17 Nov 2023 09:12:18 +0100 Subject: [PATCH 70/86] force tests --- pallets/dapp-staking-v3/src/test/tests.rs | 160 +++++++++++++++++++++- 1 file changed, 159 insertions(+), 1 deletion(-) diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index bf1b005f8e..de96280221 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -663,7 +663,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { assert_unlock(account, unlock_amount); } - // We can still unlock in the current erblocka, theoretically + // We can still unlock in the current era, theoretically for _ in 0..5 { assert_unlock(account, unlock_amount); } @@ -1824,6 +1824,164 @@ fn cleanup_expired_entries_fails_with_no_entries() { }) } +#[test] +fn force_era_works() { + ExtBuilder::build().execute_with(|| { + // 1. Force new era in the voting subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert_eq!( + init_state.subperiod(), + Subperiod::Voting, + "Sanity check." + ); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.period_end_era(), + ); + + // Go to the next block, and ensure new era is started + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + ); + + // 2. Force new era in the build&earn subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.period_end_era(), + "Only era is bumped, but we don't expect to switch over to the next subperiod." + ); + + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "We're expected to remain in the same subperiod." + ); + }) +} + +#[test] +fn force_subperiod_works() { + ExtBuilder::build().execute_with(|| { + // 1. Force new subperiod in the voting subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert_eq!( + init_state.subperiod(), + Subperiod::Voting, + "Sanity check." + ); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.era + 1, + "The switch to the next subperiod must happen in the next era." + ); + + // Go to the next block, and ensure new era is started + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "New subperiod must be started." + ); + assert_eq!(ActiveProtocolState::::get().period_number(), init_state.period_number(), "Period must remain the same."); + + // 2. Force new era in the build&earn subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.era + 1, + "The switch to the next subperiod must happen in the next era." + ); + + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting, + "New subperiod must be started." + ); + assert_eq!(ActiveProtocolState::::get().period_number(), init_state.period_number() + 1, "New period must be started."); + }) +} + +#[test] +fn force_with_incorrect_origin_fails() { + ExtBuilder::build().execute_with(|| { + assert_noop!( + DappStaking::force(RuntimeOrigin::signed(1), ForcingType::Era), + BadOrigin + ); + }) +} + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// From a0725e4827af586a37a64f216baba8573430ece4 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 17 Nov 2023 09:56:47 +0100 Subject: [PATCH 71/86] Remove TODOs --- pallets/dapp-staking-v3/src/lib.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 2409bf0ba0..f0365254a3 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -71,9 +71,7 @@ mod dsv3_weight; // Lock identifier for the dApp staking pallet const STAKING_ID: LockIdentifier = *b"dapstake"; -const LOG: &str = "dapp-staking"; - -// TODO: add tracing! +const LOG_TARGET: &str = "dapp-staking"; #[frame_support::pallet] pub mod pallet { @@ -631,7 +629,7 @@ pub mod pallet { if let Err(_) = span.push(current_era, era_reward) { // This must never happen but we log the error just in case. log::error!( - target: LOG, + target: LOG_TARGET, "Failed to push era {} into the era reward span.", current_era ); @@ -1557,7 +1555,6 @@ pub mod pallet { #[pallet::call_index(16)] #[pallet::weight(Weight::zero())] pub fn force(origin: OriginFor, force_type: ForcingType) -> DispatchResult { - // TODO: tests are missing but will be added later. Self::ensure_pallet_enabled()?; T::ManagerOrigin::ensure_origin(origin)?; @@ -1627,7 +1624,7 @@ pub mod pallet { /// `true` if smart contract is active, `false` if it has been unregistered. pub(crate) fn is_active(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) - .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) + .map_or(false, |dapp_info| dapp_info.is_active()) } /// Calculates the `EraRewardSpan` index for the specified era. @@ -1711,9 +1708,6 @@ pub mod pallet { // 4. // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). - // TODO & Idea: perhaps use BTreeMap instead? It will "sort" automatically based on dApp Id, and we can efficiently remove entries, - // reducing PoV size step by step. - // It's a trade-off between speed and PoV size. Although both are quite minor, so maybe it doesn't matter that much. dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); // 5. Calculate rewards. From cafb67b62db285f4e939650eb554ed1318779d2c Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 17 Nov 2023 14:14:45 +0100 Subject: [PATCH 72/86] Tier assignment test WIP --- pallets/dapp-staking-v3/src/lib.rs | 2 + pallets/dapp-staking-v3/src/test/mock.rs | 6 +- pallets/dapp-staking-v3/src/test/tests.rs | 104 +++++++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 8 ++ 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index f0365254a3..baf5c72125 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1706,6 +1706,8 @@ pub mod pallet { tier_id.saturating_inc(); } + // TODO: what if multiple dApps satisfy the tier entry threshold but there's not enough slots to accomodate them all? + // 4. // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index d03a5f7507..cba1701567 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -230,15 +230,15 @@ impl ExtBuilder { TierThreshold::DynamicTvlAmount { amount: 100, minimum_amount: 80 }, TierThreshold::DynamicTvlAmount { amount: 50, minimum_amount: 40 }, TierThreshold::DynamicTvlAmount { amount: 20, minimum_amount: 20 }, - TierThreshold::FixedTvlAmount { amount: 10 }, + TierThreshold::FixedTvlAmount { amount: 15 }, ]) .unwrap(), }; // Init tier config, based on the initial params let init_tier_config = TiersConfiguration::<::NumberOfTiers> { - number_of_slots: 100, - slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + number_of_slots: 40, + slots_per_tier: BoundedVec::try_from(vec![2, 5, 13, 20]).unwrap(), reward_portion: tier_params.reward_portion.clone(), tier_thresholds: tier_params.tier_thresholds.clone(), }; diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index de96280221..18566dc52c 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -20,10 +20,14 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, - IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, + IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, TierConfig, }; -use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; +use frame_support::{ + assert_noop, assert_ok, + error::BadOrigin, + traits::{Currency, Get}, +}; use sp_runtime::traits::Zero; #[test] @@ -1982,6 +1986,102 @@ fn force_with_incorrect_origin_fails() { }) } +#[test] +fn get_dapp_tier_assignment_basic_example_works() { + ExtBuilder::build().execute_with(|| { + // This test will rely on the configuration inside the mock file. + // If that changes, this test will have to be updated as well. + + // Scenario: + // - 1st tier is filled up, with one dApp satisfying the threshold but not making it due to lack of tier capacity + // - 2nd tier has 2 dApps - 1 that could make it into the 1st tier and one that's supposed to be in the 2nd tier + // - 3rd tier has no dApps + // - 4th tier has 2 dApps + // - 1 dApp doesn't make it into any tier + + // Register smart contracts + let tier_config = TierConfig::::get(); + let number_of_smart_contracts = tier_config.slots_per_tier[0] + 1 + 1 + 0 + 2 + 1; + let smart_contracts: Vec<_> = (1..=number_of_smart_contracts) + .map(|x| { + let smart_contract = MockSmartContract::Wasm(x.into()); + assert_register(x.into(), &smart_contract); + smart_contract + }) + .collect(); + let mut dapp_index: usize = 0; + + fn lock_and_stake(account: usize, smart_contract: &MockSmartContract, amount: Balance) { + let account = account.try_into().unwrap(); + Balances::make_free_balance_be(&account, amount); + assert_lock(account, amount); + assert_stake(account, smart_contract, amount); + } + + // 1st tier is completely filled up, with 1 more dApp not making it inside + for x in 0..tier_config.slots_per_tier[0] as Balance { + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[0].threshold() + x + 1, + ); + dapp_index += 1; + } + // One that won't make it into the 1st tier. + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[0].threshold(), + ); + dapp_index += 1; + + // 2nd tier - 1 dedicated dApp + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[0].threshold() - 1, + ); + dapp_index += 1; + + // 3rd tier is empty + // 4th tier has 2 dApps + for x in 0..2 { + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[3].threshold() + x, + ); + dapp_index += 1; + } + + // One dApp doesn't make it into any tier + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[3].threshold() - 1, + ); + + // Finally, the actual test + let protocol_state = ActiveProtocolState::::get(); + let dapp_reward_pool = 1000000; + let tier_assignment = DappStaking::get_dapp_tier_assignment( + protocol_state.era, + protocol_state.period_number(), + dapp_reward_pool, + ); + + // Basic checks + let number_of_tiers: u32 = ::NumberOfTiers::get(); + assert_eq!(tier_assignment.period, protocol_state.period_number()); + assert_eq!(tier_assignment.rewards.len(), number_of_tiers as usize); + assert_eq!( + tier_assignment.dapps.len(), + number_of_smart_contracts as usize - 1, + "One contract doesn't make it into any tier." + ); + }) +} + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index bdec006f63..f6fb9746d5 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -1357,6 +1357,14 @@ impl TierThreshold { } } + /// Return threshold for the tier. + pub fn threshold(&self) -> Balance { + match self { + Self::FixedTvlAmount { amount } => *amount, + Self::DynamicTvlAmount { amount, .. } => *amount, + } + } + // TODO: maybe add a check that compares `Self` to another threshold and ensures it has lower requirements? // Could be useful to have this check as a sanity check when params are configured. } From 8e931069ef507d86d89e935203c14b78bd4f4252 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 17 Nov 2023 15:24:31 +0100 Subject: [PATCH 73/86] Finish dapp tier assignment test, fix reward calculation bug --- pallets/dapp-staking-v3/src/lib.rs | 5 ++- pallets/dapp-staking-v3/src/test/tests.rs | 45 ++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index baf5c72125..fdb76dcf03 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1716,7 +1716,10 @@ pub mod pallet { let tier_rewards = tier_config .reward_portion .iter() - .map(|percent| *percent * dapp_reward_pool) + .zip(tier_config.slots_per_tier.iter()) + .map(|(percent, slots)| { + *percent * dapp_reward_pool / >::into(*slots) + }) .collect::>(); // 6. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 18566dc52c..04b67fb447 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -2065,7 +2065,7 @@ fn get_dapp_tier_assignment_basic_example_works() { let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; let tier_assignment = DappStaking::get_dapp_tier_assignment( - protocol_state.era, + protocol_state.era + 1, protocol_state.period_number(), dapp_reward_pool, ); @@ -2079,6 +2079,49 @@ fn get_dapp_tier_assignment_basic_example_works() { number_of_smart_contracts as usize - 1, "One contract doesn't make it into any tier." ); + + // 1st tier checks + let (entry_1, entry_2) = (tier_assignment.dapps[0], tier_assignment.dapps[1]); + assert_eq!(entry_1.dapp_id, 0); + assert_eq!(entry_1.tier_id, Some(0)); + + assert_eq!(entry_2.dapp_id, 1); + assert_eq!(entry_2.tier_id, Some(0)); + + // 2nd tier checks + let (entry_3, entry_4) = (tier_assignment.dapps[2], tier_assignment.dapps[3]); + assert_eq!(entry_3.dapp_id, 2); + assert_eq!(entry_3.tier_id, Some(1)); + + assert_eq!(entry_4.dapp_id, 3); + assert_eq!(entry_4.tier_id, Some(1)); + + // 4th tier checks + let (entry_5, entry_6) = (tier_assignment.dapps[4], tier_assignment.dapps[5]); + assert_eq!(entry_5.dapp_id, 4); + assert_eq!(entry_5.tier_id, Some(3)); + + assert_eq!(entry_6.dapp_id, 5); + assert_eq!(entry_6.tier_id, Some(3)); + + // Sanity check - last dapp should not exists in the tier assignment + assert!(tier_assignment + .dapps + .binary_search_by(|x| x.dapp_id.cmp(&(dapp_index.try_into().unwrap()))) + .is_err()); + + // Check that rewards are calculated correctly + tier_config + .reward_portion + .iter() + .zip(tier_config.slots_per_tier.iter()) + .enumerate() + .for_each(|(idx, (reward_portion, slots))| { + let total_tier_allocation = *reward_portion * dapp_reward_pool; + let tier_reward: Balance = total_tier_allocation / (*slots as Balance); + + assert_eq!(tier_assignment.rewards[idx], tier_reward,); + }); }) } From b7ba462d1a19a2c1f35c65463087a91c4a04ef2e Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 20 Nov 2023 14:29:22 +0100 Subject: [PATCH 74/86] Some renaming, test utils for era change WIP --- pallets/dapp-staking-v3/coverage.sh | 5 +- pallets/dapp-staking-v3/src/benchmarking.rs | 2 +- pallets/dapp-staking-v3/src/lib.rs | 34 ++--- pallets/dapp-staking-v3/src/test/mock.rs | 35 ++++- .../dapp-staking-v3/src/test/testing_utils.rs | 121 ++++++++++++++++++ pallets/dapp-staking-v3/src/test/tests.rs | 9 +- 6 files changed, 175 insertions(+), 31 deletions(-) diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh index a74d0ec2b4..46710a672c 100755 --- a/pallets/dapp-staking-v3/coverage.sh +++ b/pallets/dapp-staking-v3/coverage.sh @@ -12,4 +12,7 @@ done # Also need to check the coverage when only running extrinsic tests (disable type tests) -# Also need similar approach to extrinsic testing, as above \ No newline at end of file +# Also need similar approach to extrinsic testing, as above + + +# NOTE: this script will be deleted before the final release! \ No newline at end of file diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index a3a561a1bc..2485c18411 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -91,7 +91,7 @@ const NUMBER_OF_SLOTS: u32 = 100; pub fn initial_config() { let era_length = T::StandardEraLength::get(); - let voting_period_length_in_eras = T::StandardErasPerVotingPeriod::get(); + let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); // Init protocol state ActiveProtocolState::::put(ProtocolState { diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index fdb76dcf03..7acf6e478b 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -120,16 +120,16 @@ pub mod pallet { #[pallet::constant] type StandardEraLength: Get; - /// Length of the `Voting` period in standard eras. - /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras + /// Length of the `Voting` subperiod in standard eras. + /// Although `Voting` subperiod only consumes one 'era', we still measure its length in standard eras /// for the sake of simplicity & consistency. #[pallet::constant] - type StandardErasPerVotingPeriod: Get; + type StandardErasPerVotingSubperiod: Get; - /// Length of the `Build&Earn` period in standard eras. - /// Each `Build&Earn` period consists of one or more distinct standard eras. + /// Length of the `Build&Earn` subperiod in standard eras. + /// Each `Build&Earn` subperiod consists of one or more distinct standard eras. #[pallet::constant] - type StandardErasPerBuildAndEarnPeriod: Get; + type StandardErasPerBuildAndEarnSubperiod: Get; /// Maximum length of a single era reward span length entry. #[pallet::constant] @@ -518,13 +518,15 @@ pub mod pallet { return T::DbWeight::get().reads(1); } + // At this point it's clear that an era change will happen let mut era_info = CurrentEraInfo::::get(); let current_era = protocol_state.era; let next_era = current_era.saturating_add(1); let (maybe_period_event, era_reward) = match protocol_state.subperiod() { + // Voting subperiod only lasts for one 'prolonged' era Subperiod::Voting => { - // For the sake of consistency, we put zero reward into storage + // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod. let era_reward = EraReward { staker_reward_pool: Balance::zero(), staked: era_info.total_staked_amount(), @@ -532,7 +534,7 @@ pub mod pallet { }; let subperiod_end_era = - next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); + next_era.saturating_add(T::StandardErasPerBuildAndEarnSubperiod::get()); let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); protocol_state @@ -636,19 +638,6 @@ pub mod pallet { } EraRewards::::insert(&era_span_index, span); - // TODO: maybe do lazy history cleanup in this function? - // if let Some(expired_period) = Self::oldest_claimable_period().checked_sub(1) { - // if let Some(expired_period_info) = PeriodEnd::::get(&expired_period) { - // let final_era = expired_period_info.final_era; - // let expired_era_span_index = Self::era_reward_span_index(final_era); - - // let era_reward_span = EraRewards::::get(&expired_era_span_index) - // .unwrap_or_default(); - // if era_reward_span.last_era() - - // } - // } - Self::deposit_event(Event::::NewEra { era: next_era }); if let Some(period_event) = maybe_period_event { Self::deposit_event(period_event); @@ -1618,7 +1607,8 @@ pub mod pallet { /// Returns the number of blocks per voting period. pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { - T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) + T::StandardEraLength::get() + .saturating_mul(T::StandardErasPerVotingSubperiod::get().into()) } /// `true` if smart contract is active, `false` if it has been unregistered. diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index cba1701567..3968fc849d 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -16,7 +16,11 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use crate::{self as pallet_dapp_staking, *}; +use crate::{ + self as pallet_dapp_staking, + test::testing_utils::{assert_block_bump, MemorySnapshot}, + *, +}; use frame_support::{ construct_runtime, parameter_types, @@ -26,14 +30,13 @@ use frame_support::{ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; +use sp_io::TestExternalities; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, Permill, }; -use sp_io::TestExternalities; - pub(crate) type AccountId = u64; pub(crate) type BlockNumber = u64; pub(crate) type Balance = u128; @@ -155,8 +158,8 @@ impl pallet_dapp_staking::Config for Test { type NativePriceProvider = DummyPriceProvider; type RewardPoolProvider = DummyRewardPoolProvider; type StandardEraLength = ConstU64<10>; - type StandardErasPerVotingPeriod = ConstU32<8>; - type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; + type StandardErasPerVotingSubperiod = ConstU32<8>; + type StandardErasPerBuildAndEarnSubperiod = ConstU32<16>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<10>; @@ -195,7 +198,7 @@ impl ExtBuilder { let era_length: BlockNumber = <::StandardEraLength as sp_core::Get<_>>::get(); let voting_period_length_in_eras: EraNumber = - <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( + <::StandardErasPerVotingSubperiod as sp_core::Get<_>>::get( ); // Init protocol state @@ -209,6 +212,23 @@ impl ExtBuilder { }, maintenance: false, }); + pallet_dapp_staking::CurrentEraInfo::::put(EraInfo { + total_locked: 0, + unlocking: 0, + current_stake_amount: StakeAmount { + voting: 0, + build_and_earn: 0, + era: 1, + period: 1, + }, + next_stake_amount: StakeAmount { + voting: 0, + build_and_earn: 0, + era: 2, + period: 1, + }, + + }); // Init tier params let tier_params = TierParameters::<::NumberOfTiers> { @@ -263,7 +283,10 @@ pub(crate) fn run_to_block(n: u64) { DappStaking::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); // This is performed outside of dapps staking but we expect it before on_initialize + + let pre_snapshot = MemorySnapshot::new(); DappStaking::on_initialize(System::block_number()); + assert_block_bump(&pre_snapshot); } } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 371fbe343f..28c6ff5645 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -1114,6 +1114,127 @@ pub(crate) fn assert_cleanup_expired_entries(account: AccountId) { ); } +/// Asserts correct transitions of the protocol after a block has been produced. +pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { + let current_block_number = System::block_number(); + + // No checks if era didn't change. + if pre_snapshot.active_protocol_state.next_era_start > current_block_number { + return; + } + + // Verify post state + let post_snapshot = MemorySnapshot::new(); + + let is_new_subperiod = pre_snapshot + .active_protocol_state + .period_info + .subperiod_end_era + <= post_snapshot.active_protocol_state.era; + + // 1. Verify protocol state + let pre_protoc_state = pre_snapshot.active_protocol_state; + let post_protoc_state = post_snapshot.active_protocol_state; + assert_eq!(post_protoc_state.era, pre_protoc_state.era + 1); + + match pre_protoc_state.subperiod() { + Subperiod::Voting => { + assert_eq!( + post_protoc_state.subperiod(), + Subperiod::BuildAndEarn, + "Voting subperiod only lasts for a single era." + ); + + let eras_per_bep: EraNumber = + ::StandardErasPerBuildAndEarnSubperiod::get(); + assert_eq!( + post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.era + eras_per_bep, + "Build&earn must last for the predefined amount of standard eras." + ); + + let standard_era_length: BlockNumber = ::StandardEraLength::get(); + assert_eq!( + post_protoc_state.next_era_start, + current_block_number + standard_era_length, + "Era in build&earn period must last for the predefined amount of blocks." + ); + } + Subperiod::BuildAndEarn => { + if is_new_subperiod { + assert_eq!( + post_protoc_state.subperiod(), + Subperiod::Voting, + "Since we expect a new subperiod, it must be 'Voting'." + ); + assert_eq!( + post_protoc_state.period_number(), + pre_protoc_state.period_number() + 1, + "Ending 'Build&Earn' triggers a new period." + ); + assert_eq!( + post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.era + 1, + "Voting era must last for a single era." + ); + + let blocks_per_standard_era: BlockNumber = + ::StandardEraLength::get(); + let eras_per_voting_subperiod: EraNumber = + ::StandardErasPerVotingSubperiod::get(); + let eras_per_voting_subperiod: BlockNumber = eras_per_voting_subperiod.into(); + let era_length: BlockNumber = blocks_per_standard_era * eras_per_voting_subperiod; + assert_eq!( + post_protoc_state.next_era_start, + current_block_number + era_length, + "The upcoming 'Voting' subperiod must last for the 'standard eras per voting subperiod x standard era length' amount of blocks." + ); + } else { + assert_eq!( + post_protoc_state.period_info, pre_protoc_state.period_info, + "New subperiod hasn't started, hence it should remain 'Build&Earn'." + ); + } + } + } + + // 2. Verify current era info + let pre_era_info = pre_snapshot.current_era_info; + let post_era_info = post_snapshot.current_era_info; + + assert_eq!(post_era_info.total_locked, pre_era_info.total_locked); + assert_eq!(post_era_info.unlocking, pre_era_info.unlocking); + + // New period has started + if is_new_subperiod && pre_protoc_state.subperiod() == Subperiod::BuildAndEarn { + assert!(post_era_info.current_stake_amount.is_empty()); + assert!(post_era_info.next_stake_amount.is_empty()); + } else { + assert_eq!( + post_era_info.current_stake_amount, + pre_era_info.next_stake_amount + ); + assert_eq!( + post_era_info.next_stake_amount.total(), + post_era_info.current_stake_amount.total() + ); + println!("PostEraInfo next {:?}", post_era_info); + println!("PostProtocState: {:?}", post_protoc_state); + println!("===================="); + // assert_eq!( + // post_era_info.next_stake_amount.era, + // post_protoc_state.era + 1, + // ); + // continue here - need to add logic to update to correct era + } + + // TODO + // - check current era info + // - check tier config + // - check era reward + // - check events +} + /// Returns from which starting era to which ending era can rewards be claimed for the specified account. /// /// If `None` is returned, there is nothing to claim. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 04b67fb447..b4eb205b03 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -175,6 +175,13 @@ fn maintenace_mode_call_filtering_works() { }) } +#[test] +fn on_initialize_is_noop_if_no_era_change() { + ExtBuilder::build().execute_with(|| { + // TODO + }) +} + #[test] fn on_initialize_state_change_works() { ExtBuilder::build().execute_with(|| { @@ -212,7 +219,7 @@ fn on_initialize_state_change_works() { // Advance eras just until we reach the next voting period let eras_per_bep_period: EraNumber = - ::StandardErasPerBuildAndEarnPeriod::get(); + ::StandardErasPerBuildAndEarnSubperiod::get(); let blocks_per_era: BlockNumber = ::StandardEraLength::get(); for era in 2..(2 + eras_per_bep_period - 1) { let pre_block = System::block_number(); From db6cd3607ec2a9d246d887aac4ccf2d883109e71 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 20 Nov 2023 19:30:34 +0100 Subject: [PATCH 75/86] Fix minor issues, propagaste renaming --- .../dapp-staking-v3/src/test/testing_utils.rs | 37 +++++++++++++------ .../dapp-staking-v3/src/test/tests_types.rs | 20 +++++++++- pallets/dapp-staking-v3/src/types.rs | 8 +++- runtime/local/src/lib.rs | 4 +- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 28c6ff5645..650e775e61 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -1207,8 +1207,24 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { // New period has started if is_new_subperiod && pre_protoc_state.subperiod() == Subperiod::BuildAndEarn { - assert!(post_era_info.current_stake_amount.is_empty()); - assert!(post_era_info.next_stake_amount.is_empty()); + assert_eq!( + post_era_info.current_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: pre_protoc_state.era + 1, + period: pre_protoc_state.period_number() + 1, + } + ); + assert_eq!( + post_era_info.next_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: pre_protoc_state.era + 2, + period: pre_protoc_state.period_number() + 1, + } + ); } else { assert_eq!( post_era_info.current_stake_amount, @@ -1218,18 +1234,17 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { post_era_info.next_stake_amount.total(), post_era_info.current_stake_amount.total() ); - println!("PostEraInfo next {:?}", post_era_info); - println!("PostProtocState: {:?}", post_protoc_state); - println!("===================="); - // assert_eq!( - // post_era_info.next_stake_amount.era, - // post_protoc_state.era + 1, - // ); - // continue here - need to add logic to update to correct era + assert_eq!( + post_era_info.next_stake_amount.era, + post_protoc_state.era + 1, + ); + assert_eq!( + post_era_info.next_stake_amount.period, + pre_era_info.period_number(), + ); } // TODO - // - check current era info // - check tier config // - check era reward // - check events diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index b83c1d8704..201276362f 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1867,8 +1867,24 @@ fn era_info_migrate_to_next_era_works() { assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); - assert!(era_info.current_stake_amount.is_empty()); - assert!(era_info.next_stake_amount.is_empty()); + assert_eq!( + era_info.current_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: era_info_snapshot.current_stake_amount.era + 1, + period: era_info_snapshot.current_stake_amount.period + 1, + } + ); + assert_eq!( + era_info.next_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: era_info_snapshot.current_stake_amount.era + 2, + period: era_info_snapshot.current_stake_amount.period + 1, + } + ); } } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index f6fb9746d5..82cc32e35a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -940,8 +940,12 @@ impl EraInfo { match next_subperiod { // If next era marks start of new voting period period, it means we're entering a new period Some(Subperiod::Voting) => { - self.current_stake_amount = Default::default(); - self.next_stake_amount = Default::default(); + for stake_amount in [&mut self.current_stake_amount, &mut self.next_stake_amount] { + stake_amount.voting = Zero::zero(); + stake_amount.build_and_earn = Zero::zero(); + stake_amount.era.saturating_inc(); + stake_amount.period.saturating_inc(); + } } Some(Subperiod::BuildAndEarn) | None => { self.current_stake_amount = self.next_stake_amount; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index d3ee064fb1..ab8217a9ff 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -479,8 +479,8 @@ impl pallet_dapp_staking_v3::Config for Runtime { type NativePriceProvider = DummyPriceProvider; type RewardPoolProvider = DummyRewardPoolProvider; type StandardEraLength = ConstU32<30>; // should be 1 minute per standard era - type StandardErasPerVotingPeriod = ConstU32<2>; - type StandardErasPerBuildAndEarnPeriod = ConstU32<10>; + type StandardErasPerVotingSubperiod = ConstU32<2>; + type StandardErasPerBuildAndEarnSubperiod = ConstU32<10>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<100>; From b940375b1166ac16f0093cc68b971a064cd8ad68 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 20 Nov 2023 19:45:11 +0100 Subject: [PATCH 76/86] More checks --- .../dapp-staking-v3/src/test/testing_utils.rs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 650e775e61..48cf2f3929 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -20,8 +20,8 @@ use crate::test::mock::*; use crate::types::*; use crate::{ pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, - DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, PeriodEndInfo, - StakerInfo, + DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, PeriodEnd, + PeriodEndInfo, StakerInfo, TierConfig, }; use frame_support::{assert_ok, traits::Get}; @@ -51,6 +51,8 @@ pub(crate) struct MemorySnapshot { era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, dapp_tiers: HashMap>, + tier_config: TiersConfiguration<::NumberOfTiers>, + next_tier_config: TiersConfiguration<::NumberOfTiers>, } impl MemorySnapshot { @@ -69,6 +71,8 @@ impl MemorySnapshot { era_rewards: EraRewards::::iter().collect(), period_end: PeriodEnd::::iter().collect(), dapp_tiers: DAppTiers::::iter().collect(), + tier_config: TierConfig::::get(), + next_tier_config: NextTierConfig::::get(), } } @@ -1240,12 +1244,26 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { ); assert_eq!( post_era_info.next_stake_amount.period, - pre_era_info.period_number(), + pre_protoc_state.period_number(), ); } + // 3. Verify tier config + match pre_protoc_state.subperiod() { + Subperiod::Voting => { + assert!(!NextTierConfig::::exists()); + assert_eq!(post_snapshot.tier_config, pre_snapshot.next_tier_config); + } + Subperiod::BuildAndEarn if is_new_subperiod => { + assert!(NextTierConfig::::exists()); + assert_eq!(post_snapshot.tier_config, pre_snapshot.tier_config); + } + _ => { + assert_eq!(post_snapshot.tier_config, pre_snapshot.tier_config); + } + } + // TODO - // - check tier config // - check era reward // - check events } From 878e450c3b6cfc50e44e8750534d1f6d84d02c2b Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 10:32:54 +0100 Subject: [PATCH 77/86] Finish on_init tests --- pallets/dapp-staking-v3/src/lib.rs | 8 +-- .../dapp-staking-v3/src/test/testing_utils.rs | 70 ++++++++++++++++++- pallets/dapp-staking-v3/src/test/tests.rs | 23 ++++-- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 7acf6e478b..aab19d9faa 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -181,8 +181,8 @@ pub mod pallet { NewEra { era: EraNumber, }, - /// New period has started. - NewPeriod { + /// New subperiod has started. + NewSubperiod { subperiod: Subperiod, number: PeriodNumber, }, @@ -547,7 +547,7 @@ pub mod pallet { TierConfig::::put(next_tier_config); ( - Some(Event::::NewPeriod { + Some(Event::::NewSubperiod { subperiod: protocol_state.subperiod(), number: protocol_state.period_number(), }), @@ -603,7 +603,7 @@ pub mod pallet { NextTierConfig::::put(new_tier_config); ( - Some(Event::::NewPeriod { + Some(Event::::NewSubperiod { subperiod: protocol_state.subperiod(), number: protocol_state.period_number(), }), diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 48cf2f3929..14b3fa0bad 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -1263,9 +1263,73 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { } } - // TODO - // - check era reward - // - check events + // 4. Verify era reward + let era_span_index = DappStaking::era_reward_span_index(pre_protoc_state.era); + let maybe_pre_era_reward_span = pre_snapshot.era_rewards.get(&era_span_index); + let post_era_reward_span = post_snapshot + .era_rewards + .get(&era_span_index) + .expect("Era reward info must exist after era has finished."); + + // Sanity check + if let Some(pre_era_reward_span) = maybe_pre_era_reward_span { + assert_eq!( + pre_era_reward_span.last_era(), + pre_protoc_state.era - 1, + "If entry exists, it should cover eras up to the previous one, exactly." + ); + } + + assert_eq!( + post_era_reward_span.last_era(), + pre_protoc_state.era, + "Entry must cover the current era." + ); + assert_eq!( + post_era_reward_span + .get(pre_protoc_state.era) + .expect("Above check proved it must exist.") + .staked, + pre_snapshot.current_era_info.total_staked_amount(), + "Total staked amount must be equal to total amount staked at the end of the era." + ); + + // 5. Verify period end + if is_new_subperiod && pre_protoc_state.subperiod() == Subperiod::BuildAndEarn { + let period_end_info = post_snapshot.period_end[&pre_protoc_state.period_number()]; + assert_eq!( + period_end_info.total_vp_stake, + pre_snapshot + .current_era_info + .staked_amount(Subperiod::Voting), + ); + } + + // 6. Verify event(s) + if is_new_subperiod { + let events = dapp_staking_events(); + assert!( + events.len() >= 2, + "At least 2 events should exist from era & subperiod change." + ); + assert_eq!( + events[events.len() - 2], + Event::NewEra { + era: post_protoc_state.era, + } + ); + assert_eq!( + events[events.len() - 1], + Event::NewSubperiod { + subperiod: pre_protoc_state.subperiod().next(), + number: post_protoc_state.period_number(), + } + ) + } else { + System::assert_last_event(RuntimeEvent::DappStaking(Event::NewEra { + era: post_protoc_state.era, + })); + } } /// Returns from which starting era to which ending era can rewards be claimed for the specified account. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index b4eb205b03..c5df0ee509 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -24,12 +24,13 @@ use crate::{ }; use frame_support::{ - assert_noop, assert_ok, + assert_noop, assert_ok, assert_storage_noop, error::BadOrigin, - traits::{Currency, Get}, + traits::{Currency, Get, OnFinalize, OnInitialize}, }; use sp_runtime::traits::Zero; +// TODO: remove this prior to the merge #[test] fn print_test() { ExtBuilder::build().execute_with(|| { @@ -178,15 +179,25 @@ fn maintenace_mode_call_filtering_works() { #[test] fn on_initialize_is_noop_if_no_era_change() { ExtBuilder::build().execute_with(|| { - // TODO + let protocol_state = ActiveProtocolState::::get(); + let current_block_number = System::block_number(); + + assert!( + current_block_number < protocol_state.next_era_start, + "Sanity check, otherwise test doesn't make sense." + ); + + // Sanity check + assert_storage_noop!(DappStaking::on_finalize(current_block_number)); + + // If no era change, on_initialize should be a noop + assert_storage_noop!(DappStaking::on_initialize(current_block_number + 1)); }) } #[test] -fn on_initialize_state_change_works() { +fn on_initialize_base_state_change_works() { ExtBuilder::build().execute_with(|| { - // TODO: test `EraInfo` change and verify events. This would be good to do each time we call the helper functions to go to next era or period. - // Sanity check let protocol_state = ActiveProtocolState::::get(); assert_eq!(protocol_state.era, 1); From 215a13ea170b4a849505a5f206814ea9cb6e0006 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 12:25:17 +0100 Subject: [PATCH 78/86] Comments, docs, some benchmarks --- pallets/dapp-staking-v3/src/benchmarking.rs | 119 ++++++++++++++++++ pallets/dapp-staking-v3/src/lib.rs | 38 ++++-- .../dapp-staking-v3/src/test/tests_types.rs | 8 +- pallets/dapp-staking-v3/src/types.rs | 1 - 4 files changed, 152 insertions(+), 14 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 2485c18411..ed4fdd6a52 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -89,6 +89,13 @@ const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; const NUMBER_OF_SLOTS: u32 = 100; +const SEED: u32 = 9000; + +/// Assert that the last event equals the provided one. +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + pub fn initial_config() { let era_length = T::StandardEraLength::get(); let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); @@ -165,6 +172,118 @@ fn max_number_of_contracts() -> u32 { mod benchmarks { use super::*; + #[benchmark] + fn maintenance_mode() { + initial_config::(); + + #[extrinsic_call] + _(RawOrigin::Root, true); + + assert_last_event::(Event::::MaintenanceMode { enabled: true }.into()); + } + + #[benchmark] + fn register() { + initial_config::(); + + let account: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + + #[extrinsic_call] + _(RawOrigin::Root, account.clone(), smart_contract.clone()); + + assert_last_event::( + Event::::DAppRegistered { + owner: account, + smart_contract, + dapp_id: 0, + } + .into(), + ); + } + + #[benchmark] + fn set_dapp_reward_beneficiary() { + initial_config::(); + + let owner: T::AccountId = whitelisted_caller(); + let beneficiary: Option = Some(account("beneficiary", 0, SEED)); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(owner), + smart_contract.clone(), + beneficiary.clone(), + ); + + assert_last_event::( + Event::::DAppRewardDestinationUpdated { + smart_contract, + beneficiary, + } + .into(), + ); + } + + #[benchmark] + fn set_dapp_owner() { + initial_config::(); + + let init_owner: T::AccountId = whitelisted_caller(); + let new_owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + init_owner.clone().into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(init_owner), + smart_contract.clone(), + new_owner.clone(), + ); + + assert_last_event::( + Event::::DAppOwnerChanged { + smart_contract, + new_owner, + } + .into(), + ); + } + + #[benchmark] + fn unregister() { + initial_config::(); + + let owner: T::AccountId = whitelisted_caller(); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _(RawOrigin::Root, smart_contract.clone()); + + assert_last_event::( + Event::::DAppUnregistered { + smart_contract, + era: ActiveProtocolState::::get().era, + } + .into(), + ); + } + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs #[benchmark] diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index aab19d9faa..08e8e7e3cb 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -177,10 +177,10 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event { + /// Maintenance mode has been either enabled or disabled. + MaintenanceMode { enabled: bool }, /// New era has started. - NewEra { - era: EraNumber, - }, + NewEra { era: EraNumber }, /// New subperiod has started. NewSubperiod { subperiod: Subperiod, @@ -245,12 +245,14 @@ pub mod pallet { era: EraNumber, amount: Balance, }, + /// Bonus reward has been paid out to a loyal staker. BonusReward { account: T::AccountId, smart_contract: T::SmartContract, period: PeriodNumber, amount: Balance, }, + /// dApp reward has been paid out to a beneficiary. DAppReward { beneficiary: T::AccountId, smart_contract: T::SmartContract, @@ -258,15 +260,14 @@ pub mod pallet { era: EraNumber, amount: Balance, }, + /// Account has unstaked funds from an unregistered smart contract UnstakeFromUnregistered { account: T::AccountId, smart_contract: T::SmartContract, amount: Balance, }, - ExpiredEntriesRemoved { - account: T::AccountId, - count: u16, - }, + /// Some expired stake entries have been removed from storage. + ExpiredEntriesRemoved { account: T::AccountId, count: u16 }, } #[pallet::error] @@ -657,12 +658,15 @@ pub mod pallet { pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; ActiveProtocolState::::mutate(|state| state.maintenance = enabled); + + Self::deposit_event(Event::::MaintenanceMode { enabled }); Ok(()) } /// Used to register a new contract for dApp staking. /// /// If successful, smart contract will be assigned a simple, unique numerical identifier. + /// Owner is set to be initial beneficiary & manager of the dApp. #[pallet::call_index(1)] #[pallet::weight(Weight::zero())] pub fn register( @@ -713,6 +717,7 @@ pub mod pallet { /// /// Caller has to be dApp owner. /// If set to `None`, rewards will be deposited to the dApp owner. + /// After this call, all existing & future rewards will be paid out to the beneficiary. #[pallet::call_index(2)] #[pallet::weight(Weight::zero())] pub fn set_dapp_reward_beneficiary( @@ -828,6 +833,8 @@ pub mod pallet { /// /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. + /// + /// Locked amount can immediately be used for staking. #[pallet::call_index(5)] #[pallet::weight(Weight::zero())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { @@ -981,9 +988,13 @@ pub mod pallet { } /// Stake the specified amount on a smart contract. - /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. + /// The precise `amount` specified **must** be available for staking. + /// The total amount staked on a dApp must be greater than the minimum required value. /// - /// Depending on the period type, appropriate stake amount will be updated. + /// Depending on the period type, appropriate stake amount will be updated. During `Voting` subperiod, `voting` stake amount is updated, + /// and same for `Build&Earn` subperiod. + /// + /// Staked amount is only eligible for rewards from the next era onwards. #[pallet::call_index(9)] #[pallet::weight(Weight::zero())] pub fn stake( @@ -1105,7 +1116,12 @@ pub mod pallet { /// Unstake the specified amount from a smart contract. /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. /// + /// If unstaking the specified `amount` would take staker below the minimum stake threshold, everything is unstaked. + /// /// Depending on the period type, appropriate stake amount will be updated. + /// In case amount is unstaked during `Voting` subperiod, the `voting` amount is reduced. + /// In case amount is unstaked during `Build&Earn` subperiod, first the `build_and_earn` is reduced, + /// and any spillover is subtracted from the `voting` amount. #[pallet::call_index(10)] #[pallet::weight(Weight::zero())] pub fn unstake( @@ -1207,8 +1223,7 @@ pub mod pallet { } /// Claims some staker rewards, if user has any. - /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening - /// if appropriate entries exist in account's ledger. + /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening. #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { @@ -1414,6 +1429,7 @@ pub mod pallet { } /// Used to unstake funds from a contract that was unregistered after an account staked on it. + /// This is required if staker wants to re-stake these funds on another active contract during the ongoing period. #[pallet::call_index(14)] #[pallet::weight(Weight::zero())] pub fn unstake_from_unregistered( diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 201276362f..50074ec7a6 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -2666,8 +2666,12 @@ fn tier_configuration_basic_tests() { assert!(init_config.is_valid(), "Init config must be valid!"); // Create a new config, based on a new price - let new_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD - let new_config = init_config.calculate_new(new_price, ¶ms); + let high_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(high_price, ¶ms); + assert!(new_config.is_valid()); + + let low_price = FixedU64::from_rational(1, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(low_price, ¶ms); assert!(new_config.is_valid()); // TODO: expand tests, add more sanity checks (e.g. tier 3 requirement should never be lower than tier 4, etc.) diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 82cc32e35a..1f3590a065 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -63,7 +63,6 @@ //! * `DAppTier` - a compact struct describing a dApp's tier. //! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. //! -//! TODO: some types are missing so double check before final merge that everything is covered and explained correctly use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; From 75e6fd32c2e4fe72710914c97ba824ef0ee3e64e Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 14:12:21 +0100 Subject: [PATCH 79/86] Benchmark progress --- pallets/dapp-staking-v3/src/benchmarking.rs | 245 ++++++++++++++++++-- 1 file changed, 223 insertions(+), 22 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index ed4fdd6a52..cc0971304a 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -58,28 +58,28 @@ pub(crate) fn advance_to_next_era() { advance_to_era::(ActiveProtocolState::::get().era + 1); } -// /// Advance blocks until the specified period has been reached. -// /// -// /// Function has no effect if period is already passed. -// pub(crate) fn advance_to_period(period: PeriodNumber) { -// assert!(period >= ActiveProtocolState::::get().period_number()); -// while ActiveProtocolState::::get().period_number() < period { -// run_for_blocks::(One::one()); -// } -// } - -// /// Advance blocks until next period has been reached. -// pub(crate) fn advance_to_next_period() { -// advance_to_period::(ActiveProtocolState::::get().period_number() + 1); -// } - -// /// Advance blocks until next period type has been reached. -// pub(crate) fn advance_to_next_subperiod() { -// let subperiod = ActiveProtocolState::::get().subperiod(); -// while ActiveProtocolState::::get().subperiod() == subperiod { -// run_for_blocks::(One::one()); -// } -// } +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(crate) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next period has been reached. +pub(crate) fn advance_to_next_period() { + advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period type has been reached. +pub(crate) fn advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { + run_for_blocks::(One::one()); + } +} // All our networks use 18 decimals for native currency so this should be fine. const UNIT: Balance = 1_000_000_000_000_000_000; @@ -284,8 +284,209 @@ mod benchmarks { ); } + #[benchmark] + fn lock() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), amount); + + assert_last_event::( + Event::::Locked { + account: staker, + amount, + } + .into(), + ); + } + + #[benchmark] + fn unlock() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 2; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), 1); + + assert_last_event::( + Event::::Unlocking { + account: staker, + amount: 1, + } + .into(), + ); + } + + // TODO: maybe this is not needed. Compare it after running benchmarks to the 'not-full' unlock + #[benchmark] + fn full_unlock() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 2; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + #[extrinsic_call] + unlock(RawOrigin::Signed(staker.clone()), amount); + + assert_last_event::( + Event::::Unlocking { + account: staker, + amount, + } + .into(), + ); + } + + #[benchmark] + fn claim_unlocked(x: Linear<0, { T::MaxNumberOfStakedContracts::get() }>) { + // Prepare staker account and lock some amount + let staker: T::AccountId = whitelisted_caller(); + let amount = (T::MinimumStakeAmount::get() + 1) + * Into::::into(max_number_of_contracts::()) + + 1; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Move over to the build&earn subperiod to ensure 'non-loyal' staking. + // This is needed so we can achieve staker entry cleanup after claiming unlocked tokens. + advance_to_next_subperiod::(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check - we need to stake during build&earn for entries to be cleaned up in the next era." + ); + + // Register required number of contracts and have staker stake on them. + // This is needed to achieve the cleanup functionality. + for x in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(x as u32); + let owner: T::AccountId = account("dapp_owner", x.into(), SEED); + + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + T::MinimumStakeAmount::get() + 1, + )); + } + + // Finally, unlock some amount. + let unlock_amount = 1; + assert_ok!(DappStaking::::unlock( + RawOrigin::Signed(staker.clone()).into(), + unlock_amount, + )); + + // Advance to next period to ensure the old stake entries are cleaned up. + advance_to_next_period::(); + + // Additionally, ensure enough blocks have passed so that the unlocking chunk can be claimed. + let unlock_block = Ledger::::get(&staker).unlocking[0].unlock_block; + run_to_block::(unlock_block); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone())); + + assert_last_event::( + Event::::ClaimedUnlocked { + account: staker, + amount: unlock_amount, + } + .into(), + ); + } + + #[benchmark] + fn relock_unlocking() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 2; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + let unlock_amount = 1; + assert_ok!(DappStaking::::unlock( + RawOrigin::Signed(staker.clone()).into(), + unlock_amount, + )); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone())); + + assert_last_event::( + Event::::Relock { + account: staker, + amount: unlock_amount, + } + .into(), + ); + } + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs + // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise + // - the worst case measured is usually very far off the actual value that is consumed on chain. + // There's an ongoing item to improve it (mentioned on roundtable meeting). #[benchmark] fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { // Prepare init config (protocol state, tier params & config, etc.) From 2bcb76357166834591e8cb36cd8519b87b0bda5e Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 15:28:50 +0100 Subject: [PATCH 80/86] More benchmarks --- Cargo.lock | 1 + pallets/dapp-staking-v3/Cargo.toml | 2 + pallets/dapp-staking-v3/src/benchmarking.rs | 251 ++++++++++++++++++-- pallets/dapp-staking-v3/src/lib.rs | 4 +- pallets/dapp-staking-v3/src/test/mock.rs | 10 +- 5 files changed, 244 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7a797c1f5..f69aee8ac4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7112,6 +7112,7 @@ dependencies = [ name = "pallet-dapp-staking-v3" version = "0.0.1-alpha" dependencies = [ + "assert_matches", "astar-primitives", "frame-benchmarking", "frame-support", diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index d38a23eaf1..c79755828d 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -24,6 +24,7 @@ sp-std = { workspace = true } astar-primitives = { workspace = true } +assert_matches = { workspace = true, optional = true } frame-benchmarking = { workspace = true, optional = true } [dev-dependencies] @@ -54,5 +55,6 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "astar-primitives/runtime-benchmarks", + "assert_matches", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index cc0971304a..c5a2649790 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -24,6 +24,8 @@ use frame_benchmarking::v2::*; use frame_support::assert_ok; use frame_system::{Pallet as System, RawOrigin}; +use ::assert_matches::assert_matches; + // TODO: copy/paste from mock, make it more generic later /// Run to the specified block number. @@ -96,6 +98,15 @@ fn assert_last_event(generic_event: ::RuntimeEvent) { frame_system::Pallet::::assert_last_event(generic_event.into()); } +// Return all dApp staking events from the event buffer. +fn dapp_staking_events() -> Vec> { + System::::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() +} + pub fn initial_config() { let era_length = T::StandardEraLength::get(); let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); @@ -383,7 +394,7 @@ mod benchmarks { let staker: T::AccountId = whitelisted_caller(); let amount = (T::MinimumStakeAmount::get() + 1) * Into::::into(max_number_of_contracts::()) - + 1; + + Into::::into(T::MaxUnlockingChunks::get()); T::Currency::make_free_balance_be(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), @@ -401,9 +412,9 @@ mod benchmarks { // Register required number of contracts and have staker stake on them. // This is needed to achieve the cleanup functionality. - for x in 0..x { - let smart_contract = T::BenchmarkHelper::get_smart_contract(x as u32); - let owner: T::AccountId = account("dapp_owner", x.into(), SEED); + for idx in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(idx as u32); + let owner: T::AccountId = account("dapp_owner", idx.into(), SEED); assert_ok!(DappStaking::::register( RawOrigin::Root.into(), @@ -418,18 +429,30 @@ mod benchmarks { )); } - // Finally, unlock some amount. + // Unlock some amount - but we want to fill up the whole vector with chunks. let unlock_amount = 1; - assert_ok!(DappStaking::::unlock( - RawOrigin::Signed(staker.clone()).into(), - unlock_amount, - )); + for _ in 0..T::MaxUnlockingChunks::get() { + assert_ok!(DappStaking::::unlock( + RawOrigin::Signed(staker.clone()).into(), + unlock_amount, + )); + run_for_blocks::(One::one()); + } + assert_eq!( + Ledger::::get(&staker).unlocking.len(), + T::MaxUnlockingChunks::get() as usize + ); + let unlock_amount = unlock_amount * Into::::into(T::MaxUnlockingChunks::get()); // Advance to next period to ensure the old stake entries are cleaned up. advance_to_next_period::(); // Additionally, ensure enough blocks have passed so that the unlocking chunk can be claimed. - let unlock_block = Ledger::::get(&staker).unlocking[0].unlock_block; + let unlock_block = Ledger::::get(&staker) + .unlocking + .last() + .expect("At least one entry must exist.") + .unlock_block; run_to_block::(unlock_block); #[extrinsic_call] @@ -457,18 +480,24 @@ mod benchmarks { smart_contract.clone(), )); - let amount = T::MinimumLockedAmount::get() * 2; + let amount = + T::MinimumLockedAmount::get() * 2 + Into::::into(T::MaxUnlockingChunks::get()); T::Currency::make_free_balance_be(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, )); + // Unlock some amount - but we want to fill up the whole vector with chunks. let unlock_amount = 1; - assert_ok!(DappStaking::::unlock( - RawOrigin::Signed(staker.clone()).into(), - unlock_amount, - )); + for _ in 0..T::MaxUnlockingChunks::get() { + assert_ok!(DappStaking::::unlock( + RawOrigin::Signed(staker.clone()).into(), + unlock_amount, + )); + run_for_blocks::(One::one()); + } + let unlock_amount = unlock_amount * Into::::into(T::MaxUnlockingChunks::get()); #[extrinsic_call] _(RawOrigin::Signed(staker.clone())); @@ -482,6 +511,198 @@ mod benchmarks { ); } + #[benchmark] + fn stake() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(staker.clone()), + smart_contract.clone(), + amount, + ); + + assert_last_event::( + Event::::Stake { + account: staker, + smart_contract, + amount, + } + .into(), + ); + } + + #[benchmark] + fn unstake() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() + 1; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + let unstake_amount = 1; + + #[extrinsic_call] + _( + RawOrigin::Signed(staker.clone()), + smart_contract.clone(), + unstake_amount, + ); + + assert_last_event::( + Event::::Unstake { + account: staker, + smart_contract, + amount: unstake_amount, + } + .into(), + ); + } + + #[benchmark] + fn claim_staker_rewards_past_period(x: Linear<1, { T::EraRewardSpanLength::get() }>) { + initial_config::(); + + // Prepare staker & register smart contract + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + // Lock some amount by the staker + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Advance to the era just before a new span entry is created. + // This ensures that when rewards are claimed, we'll be claiming from the new span. + // + // This is convenient because it allows us to control how many rewards are claimed. + advance_to_era::(T::EraRewardSpanLength::get() - 1); + + // Now ensure the expected amount of rewards are claimable. + advance_to_era::( + ActiveProtocolState::::get().era + T::EraRewardSpanLength::get() - x, + ); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + // This ensures we claim from the past period. + advance_to_next_period::(); + + // For testing purposes + System::::reset_events(); + + #[extrinsic_call] + claim_staker_rewards(RawOrigin::Signed(staker.clone())); + + // No need to do precise check of values, but predetermiend amount of 'Reward' events is expected. + let dapp_staking_events = dapp_staking_events::(); + assert_eq!(dapp_staking_events.len(), x as usize); + dapp_staking_events.iter().for_each(|e| { + assert_matches!(e, Event::Reward { .. }); + }); + } + + #[benchmark] + fn claim_staker_rewards_ongoing_period(x: Linear<1, { T::EraRewardSpanLength::get() }>) { + initial_config::(); + + // Prepare staker & register smart contract + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + // Lock & stake some amount by the staker + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Advance to the era just before a new span entry is created. + // This ensures that when rewards are claimed, we'll be claiming from the new span. + // + // This is convenient because it allows us to control how many rewards are claimed. + advance_to_era::(T::EraRewardSpanLength::get() - 1); + + // Now ensure the expected amount of rewards are claimable. + advance_to_era::( + ActiveProtocolState::::get().era + T::EraRewardSpanLength::get() - x, + ); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + // This ensures we move over the entire span. + advance_to_era::(T::EraRewardSpanLength::get() * 2); + + // For testing purposes + System::::reset_events(); + + #[extrinsic_call] + claim_staker_rewards(RawOrigin::Signed(staker.clone())); + + // No need to do precise check of values, but predetermiend amount of 'Reward' events is expected. + let dapp_staking_events = dapp_staking_events::(); + assert_eq!(dapp_staking_events.len(), x as usize); + dapp_staking_events.iter().for_each(|e| { + assert_matches!(e, Event::Reward { .. }); + }); + } + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 08e8e7e3cb..bec0fc0fcb 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -92,7 +92,9 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type RuntimeEvent: From> + + IsType<::RuntimeEvent> + + TryInto>; /// Currency used for staking. /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 3968fc849d..73535acd36 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -339,12 +339,6 @@ pub fn dapp_staking_events() -> Vec> { System::events() .into_iter() .map(|r| r.event) - .filter_map(|e| { - if let RuntimeEvent::DappStaking(inner) = e { - Some(inner) - } else { - None - } - }) - .collect() + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() } From 218e6c2543a170c33788174598bc811deeb45339 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 16:31:51 +0100 Subject: [PATCH 81/86] Even more benchmarks --- pallets/dapp-staking-v3/src/benchmarking.rs | 127 ++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index c5a2649790..354378cd3f 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -107,6 +107,7 @@ fn dapp_staking_events() -> Vec> { .collect::>() } +// TODO: make it more generic per runtime? pub fn initial_config() { let era_length = T::StandardEraLength::get(); let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); @@ -703,6 +704,132 @@ mod benchmarks { }); } + #[benchmark] + fn claim_bonus_reward() { + initial_config::(); + + // Prepare staker & register smart contract + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + // Lock & stake some amount by the staker + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + // Advance to the next period so we can claim the bonus reward. + advance_to_next_period::(); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), smart_contract.clone()); + + // No need to do precise check of values, but last event must be 'BonusReward'. + assert_matches!( + dapp_staking_events::().last(), + Some(Event::BonusReward { .. }) + ); + } + + #[benchmark] + fn claim_dapp_reward() { + initial_config::(); + + // Register a dApp & stake on it. + // This is the dApp for which we'll claim rewards for. + let owner: T::AccountId = whitelisted_caller(); + let smart_contract = T::BenchmarkHelper::get_smart_contract(0); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 1000 * UNIT; + T::Currency::make_free_balance_be(&owner, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(owner.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(owner.clone()).into(), + smart_contract.clone(), + amount + )); + + // Register & stake up to max number of contracts. + // The reason is we want to have reward vector filled up to the capacity. + for idx in 1..T::MaxNumberOfContracts::get() { + let owner: T::AccountId = account("dapp_owner", idx.into(), SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(idx as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let staker: T::AccountId = account("staker", idx.into(), SEED); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + } + + // This is a hacky part to ensure we accomodate max number of contracts. + TierConfig::::mutate(|config| { + let max_number_of_contracts: u16 = T::MaxNumberOfContracts::get().try_into().unwrap(); + config.number_of_slots = max_number_of_contracts; + config.slots_per_tier[0] = max_number_of_contracts; + config.slots_per_tier[1..].iter_mut().for_each(|x| *x = 0); + }); + + // Advance enough eras so dApp reward can be claimed. + advance_to_next_subperiod::(); + advance_to_next_era::(); + let claim_era = ActiveProtocolState::::get().era - 1; + + assert_eq!( + DAppTiers::::get(claim_era) + .expect("Must exist since it's from past build&earn era.") + .dapps + .len(), + T::MaxNumberOfContracts::get() as usize, + "Sanity check to ensure we have filled up the vector completely." + ); + + #[extrinsic_call] + _( + RawOrigin::Signed(owner.clone()), + smart_contract.clone(), + claim_era, + ); + + // No need to do precise check of values, but last event must be 'DAppReward'. + assert_matches!( + dapp_staking_events::().last(), + Some(Event::DAppReward { .. }) + ); + } + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise From b8930006513e85f991ea95c3cdcfc8ec2ccf25c9 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 16:52:13 +0100 Subject: [PATCH 82/86] All extrinsics benchmarked --- pallets/dapp-staking-v3/src/benchmarking.rs | 107 +++++++++++++++++++- pallets/dapp-staking-v3/src/lib.rs | 8 +- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 354378cd3f..2321f93db0 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -26,7 +26,7 @@ use frame_system::{Pallet as System, RawOrigin}; use ::assert_matches::assert_matches; -// TODO: copy/paste from mock, make it more generic later +// TODO: make benchmar utils file and move all these helper methods there to keep this file clean(er) /// Run to the specified block number. /// Function assumes first block has been initialized. @@ -830,6 +830,111 @@ mod benchmarks { ); } + #[benchmark] + fn unstake_from_unregistered() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + assert_ok!(DappStaking::::unregister( + RawOrigin::Root.into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), smart_contract.clone()); + + assert_last_event::( + Event::::UnstakeFromUnregistered { + account: staker, + smart_contract, + amount, + } + .into(), + ); + } + + #[benchmark] + fn cleanup_expired_entries(x: Linear<1, { T::MaxNumberOfStakedContracts::get() }>) { + initial_config::(); + + // Move over to the build&earn subperiod to ensure 'non-loyal' staking. + advance_to_next_subperiod::(); + + // Prepare staker & lock some amount + let staker: T::AccountId = whitelisted_caller(); + let amount = T::MinimumLockedAmount::get() + * Into::::into(T::MaxNumberOfStakedContracts::get()); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Register dApps up the the limit + for idx in 0..x { + let owner: T::AccountId = account("dapp_owner", idx.into(), SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(idx as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + T::MinimumStakeAmount::get(), + )); + } + + // Move over to the next period, marking the entries as expired since they don't have the loyalty flag. + advance_to_next_period::(); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone())); + + assert_last_event::( + Event::::ExpiredEntriesRemoved { + account: staker, + count: x.try_into().unwrap(), + } + .into(), + ); + } + + #[benchmark] + fn force() { + initial_config::(); + + let forcing_type = ForcingType::Subperiod; + + #[extrinsic_call] + _(RawOrigin::Root, forcing_type); + + assert_last_event::(Event::::Force { forcing_type }.into()); + } + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index bec0fc0fcb..fc70d5f178 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -270,6 +270,8 @@ pub mod pallet { }, /// Some expired stake entries have been removed from storage. ExpiredEntriesRemoved { account: T::AccountId, count: u16 }, + /// Privileged origin has forced a new era and possibly a subperiod to start from next block. + Force { forcing_type: ForcingType }, } #[pallet::error] @@ -1561,7 +1563,7 @@ pub mod pallet { /// Can only be called by manager origin. #[pallet::call_index(16)] #[pallet::weight(Weight::zero())] - pub fn force(origin: OriginFor, force_type: ForcingType) -> DispatchResult { + pub fn force(origin: OriginFor, forcing_type: ForcingType) -> DispatchResult { Self::ensure_pallet_enabled()?; T::ManagerOrigin::ensure_origin(origin)?; @@ -1570,7 +1572,7 @@ pub mod pallet { let current_block = frame_system::Pallet::::block_number(); state.next_era_start = current_block.saturating_add(One::one()); - match force_type { + match forcing_type { ForcingType::Era => (), ForcingType::Subperiod => { state.period_info.subperiod_end_era = state.era.saturating_add(1); @@ -1578,6 +1580,8 @@ pub mod pallet { } }); + Self::deposit_event(Event::::Force { forcing_type }); + Ok(()) } } From 6517154ecad877adc35af0e6c550cdf9c677e7e5 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 16:56:26 +0100 Subject: [PATCH 83/86] Expand tests --- pallets/dapp-staking-v3/src/test/tests.rs | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index c5df0ee509..b0cd57cd07 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -16,10 +16,9 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use crate::test::mock::*; -use crate::test::testing_utils::*; +use crate::test::{mock::*, testing_utils::*}; use crate::{ - pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, + pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, Event, ForcingType, IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, TierConfig, }; @@ -77,11 +76,17 @@ fn maintenace_mode_works() { // Enable maintenance mode & check post-state assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::MaintenanceMode { + enabled: true, + })); assert!(ActiveProtocolState::::get().maintenance); // Call still works, even in maintenance mode - assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); - assert!(ActiveProtocolState::::get().maintenance); + assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), false)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::MaintenanceMode { + enabled: false, + })); + assert!(!ActiveProtocolState::::get().maintenance); // Incorrect origin doesn't work assert_noop!( @@ -1861,6 +1866,9 @@ fn force_era_works() { "Sanity check." ); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Era, + })); // Verify state change assert_eq!( @@ -1892,6 +1900,9 @@ fn force_era_works() { ); assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Era, + })); // Verify state change assert_eq!( @@ -1933,6 +1944,9 @@ fn force_subperiod_works() { "Sanity check." ); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Subperiod, + })); // Verify state change assert_eq!( @@ -1967,6 +1981,9 @@ fn force_subperiod_works() { ); assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Subperiod, + })); // Verify state change assert_eq!( From 076e867bf9e5c3a4e5b98e75fb7cbeaef386557a Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 21 Nov 2023 16:57:13 +0100 Subject: [PATCH 84/86] Comment --- pallets/dapp-staking-v3/src/benchmarking.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index 2321f93db0..2b04874516 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -26,7 +26,9 @@ use frame_system::{Pallet as System, RawOrigin}; use ::assert_matches::assert_matches; -// TODO: make benchmar utils file and move all these helper methods there to keep this file clean(er) +// TODO: make benchmark utils file and move all these helper methods there to keep this file clean(er) + +// TODO2: non-extrinsic calls still need to be benchmarked. /// Run to the specified block number. /// Function assumes first block has been initialized. From a99bd64f601f25bf7048af004eb11dd842b119e8 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 22 Nov 2023 16:42:31 +0100 Subject: [PATCH 85/86] Missed error test --- pallets/dapp-staking-v3/src/test/tests.rs | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index b0cd57cd07..6633aa5a7b 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1050,6 +1050,54 @@ fn stake_fails_due_to_too_small_staking_amount() { }) } +#[test] +fn stake_fails_due_to_too_many_staked_contracts() { + ExtBuilder::build().execute_with(|| { + let max_number_of_contracts: u32 = ::MaxNumberOfStakedContracts::get(); + + // Lock amount by staker + let account = 1; + assert_lock(account, 100 as Balance * max_number_of_contracts as Balance); + + // Advance to build&earn subperiod so we ensure non-loyal staking + advance_to_next_subperiod(); + + // Register smart contracts up the the max allowed number + for id in 1..=max_number_of_contracts { + let smart_contract = MockSmartContract::Wasm(id.into()); + assert_register(2, &MockSmartContract::Wasm(id.into())); + assert_stake(account, &smart_contract, 10); + } + + let excess_smart_contract = MockSmartContract::Wasm((max_number_of_contracts + 1).into()); + assert_register(2, &excess_smart_contract); + + // Max number of staked contract entries has been exceeded. + assert_noop!( + DappStaking::stake( + RuntimeOrigin::signed(account), + excess_smart_contract.clone(), + 10 + ), + Error::::TooManyStakedContracts + ); + + // Advance into next period, error should still happen + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_noop!( + DappStaking::stake( + RuntimeOrigin::signed(account), + excess_smart_contract.clone(), + 10 + ), + Error::::TooManyStakedContracts + ); + }) +} + #[test] fn unstake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { From af53a558cab6c2848dadd57084939e91c60b636c Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 29 Nov 2023 10:34:42 +0100 Subject: [PATCH 86/86] Review comments --- pallets/dapp-staking-v3/src/lib.rs | 32 ++++++++++++++++++- pallets/dapp-staking-v3/src/test/tests.rs | 38 +++++++++++++++++++++++ pallets/dapp-staking-v3/src/types.rs | 2 +- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index fc70d5f178..e282f0959f 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1657,6 +1657,32 @@ pub mod pallet { /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. /// + /// ### Algorithm + /// + /// 1. Read in over all contract stake entries. In case staked amount is zero for the current era, ignore it. + /// This information is used to calculate 'score' per dApp, which is used to determine the tier. + /// + /// 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, + /// 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. + /// + /// ```ignore + //// for each tier: + /// for each unassigned dApp: + /// if tier has capacity && dApp satisfies the tier threshold: + /// add dapp to the tier + /// else: + /// exit loop since no more dApps will satisfy the threshold since they are sorted by score + /// ``` + /// + /// 4. Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search. + /// + /// 5. 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. pub(crate) fn get_dapp_tier_assignment( era: EraNumber, @@ -1730,7 +1756,11 @@ pub mod pallet { .iter() .zip(tier_config.slots_per_tier.iter()) .map(|(percent, slots)| { - *percent * dapp_reward_pool / >::into(*slots) + if slots.is_zero() { + Zero::zero() + } else { + *percent * dapp_reward_pool / >::into(*slots) + } }) .collect::>(); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 6633aa5a7b..0795bd0653 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -2208,6 +2208,44 @@ fn get_dapp_tier_assignment_basic_example_works() { }) } +#[test] +fn get_dapp_tier_assignment_zero_slots_per_tier_works() { + ExtBuilder::build().execute_with(|| { + // This test will rely on the configuration inside the mock file. + // If that changes, this test might have to be updated as well. + + // Ensure that first tier has 0 slots. + TierConfig::::mutate(|config| { + let slots_in_first_tier = config.slots_per_tier[0]; + config.number_of_slots = config.number_of_slots - slots_in_first_tier; + config.slots_per_tier[0] = 0; + }); + + // Calculate tier assignment (we don't need dApps for this test) + let protocol_state = ActiveProtocolState::::get(); + let dapp_reward_pool = 1000000; + let tier_assignment = DappStaking::get_dapp_tier_assignment( + protocol_state.era, + protocol_state.period_number(), + dapp_reward_pool, + ); + + // Basic checks + let number_of_tiers: u32 = ::NumberOfTiers::get(); + assert_eq!(tier_assignment.period, protocol_state.period_number()); + assert_eq!(tier_assignment.rewards.len(), number_of_tiers as usize); + assert!(tier_assignment.dapps.is_empty()); + + assert!( + tier_assignment.rewards[0].is_zero(), + "1st tier has no slots so no rewards should be assigned to it." + ); + + // Regardless of that, other tiers shouldn't benefit from this + assert!(tier_assignment.rewards.iter().sum::() < dapp_reward_pool); + }) +} + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 1f3590a065..b629a82774 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -140,7 +140,7 @@ pub struct PeriodInfo { /// Period number. #[codec(compact)] pub number: PeriodNumber, - /// Subperiod ytpe. + /// Subperiod type. pub subperiod: Subperiod, /// Last era of the subperiod, after this a new subperiod should start. #[codec(compact)]