diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index f45abf32ec..e4e192c486 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -19,7 +19,7 @@ //! # dApp Staking v3 Pallet //! //! For detailed high level documentation, please refer to the attached README.md file. -//! The crate level docs will cover overal pallet structure & implementation details. +//! The crate level docs will cover overall pallet structure & implementation details. //! //! ## Overview //! @@ -27,9 +27,9 @@ //! It covers everything from locking, staking, tier configuration & assignment, reward calculation & payout. //! //! The `types` module contains all of the types used to implement the pallet. -//! All of these _types_ are exentisvely tested in their dedicated `test_types` module. +//! All of these _types_ are extensively tested in their dedicated `test_types` module. //! -//! Rest of the pallet logic is concenrated in the lib.rs file. +//! Rest of the pallet logic is concentrated in the lib.rs file. //! This logic is tested in the `tests` module, with the help of extensive `testing_utils`. //! @@ -315,7 +315,7 @@ pub mod pallet { UnavailableStakeFunds, /// 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. + /// An unexpected error occurred while trying to stake. InternalStakeError, /// Total staked amount on contract is below the minimum required value. InsufficientStakeAmount, @@ -327,7 +327,7 @@ pub mod pallet { UnstakeAmountTooLarge, /// Account has no staking information for the contract. NoStakingInfo, - /// An unexpected error occured while trying to unstake. + /// An unexpected error occurred while trying to unstake. InternalUnstakeError, /// Rewards are no longer claimable since they are too old. RewardExpired, @@ -335,18 +335,18 @@ pub mod pallet { RewardPayoutFailed, /// There are no claimable rewards. NoClaimableRewards, - /// An unexpected error occured while trying to claim staker rewards. + /// An unexpected error occurred while trying to claim staker rewards. InternalClaimStakerError, /// Account is has no eligible stake amount for bonus reward. NotEligibleForBonusReward, - /// An unexpected error occured while trying to claim bonus reward. + /// An unexpected error occurred 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, - /// An unexpected error occured while trying to claim dApp reward. + /// An unexpected error occurred while trying to claim dApp reward. InternalClaimDAppError, /// Contract is still active, not unregistered. ContractStillActive, @@ -632,7 +632,7 @@ pub mod pallet { owner: owner.clone(), id: dapp_id, state: DAppState::Registered, - reward_destination: None, + reward_beneficiary: None, }, ); @@ -671,7 +671,7 @@ pub mod pallet { ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner); - dapp_info.reward_destination = beneficiary.clone(); + dapp_info.reward_beneficiary = beneficiary.clone(); Ok(()) }, @@ -745,10 +745,7 @@ pub mod pallet { let mut dapp_info = IntegratedDApps::::get(&smart_contract).ok_or(Error::::ContractNotFound)?; - ensure!( - dapp_info.state == DAppState::Registered, - Error::::NotOperatedDApp - ); + ensure!(dapp_info.is_registered(), Error::::NotOperatedDApp); ContractStake::::remove(&dapp_info.id); @@ -1681,12 +1678,11 @@ pub mod pallet { counter.saturating_inc(); // 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, - }; - - dapp_stakes.push((dapp_id, stake_amount.total())); + if let Some(stake_amount) = stake_amount.get(era, period) { + if !stake_amount.total().is_zero() { + 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 ea1dfd18f9..83972f376b 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -110,7 +110,7 @@ pub(crate) fn assert_register(owner: AccountId, smart_contract: &MockSmartContra assert_eq!(dapp_info.state, DAppState::Registered); assert_eq!(dapp_info.owner, owner); assert_eq!(dapp_info.id, pre_snapshot.next_dapp_id); - assert!(dapp_info.reward_destination.is_none()); + assert!(dapp_info.reward_beneficiary.is_none()); assert_eq!(pre_snapshot.next_dapp_id + 1, NextDAppId::::get()); assert_eq!( @@ -142,7 +142,7 @@ pub(crate) fn assert_set_dapp_reward_beneficiary( assert_eq!( IntegratedDApps::::get(&smart_contract) .unwrap() - .reward_destination, + .reward_beneficiary, beneficiary ); } @@ -467,21 +467,49 @@ pub(crate) fn assert_stake( let post_staker_info = post_snapshot .staker_info .get(&(account, *smart_contract)) - .expect("Entry must exist since 'stake' operation was successfull."); + .expect("Entry must exist since 'stake' operation was successful."); 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 'stake' operation was successful."); let post_era_info = post_snapshot.current_era_info; // 1. verify ledger // ===================== // ===================== - assert_eq!( - post_ledger.staked, pre_ledger.staked, - "Must remain exactly the same." - ); + if is_account_ledger_expired(pre_ledger, stake_period) { + assert!( + post_ledger.staked.is_empty(), + "Must be cleaned up if expired." + ); + } else { + match pre_ledger.staked_future { + Some(stake_amount) => { + if stake_amount.era == pre_snapshot.active_protocol_state.era { + assert_eq!( + post_ledger.staked, stake_amount, + "Future entry must be moved over to the current entry." + ); + } else if stake_amount.era == pre_snapshot.active_protocol_state.era + 1 { + assert_eq!( + post_ledger.staked, pre_ledger.staked, + "Must remain exactly the same, only future must be updated." + ); + } else { + panic!("Invalid future entry era."); + } + } + None => { + assert_eq!( + post_ledger.staked, pre_ledger.staked, + "Must remain exactly the same since there's nothing to be moved." + ); + } + } + } + assert_eq!(post_ledger.staked_future.unwrap().period, stake_period); + assert_eq!(post_ledger.staked_future.unwrap().era, stake_era); assert_eq!( post_ledger.staked_amount(stake_period), pre_ledger.staked_amount(stake_period) + amount, @@ -625,7 +653,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 'unstake' operation was successfull."); + .expect("Entry must exist since 'unstake' operation was successful."); let post_era_info = post_snapshot.current_era_info; // 1. verify ledger @@ -652,9 +680,11 @@ pub(crate) fn assert_unstake( ); } else { let post_staker_info = post_snapshot - .staker_info - .get(&(account, *smart_contract)) - .expect("Entry must exist since 'stake' operation was successfull and it wasn't a full unstake."); + .staker_info + .get(&(account, *smart_contract)) + .expect( + "Entry must exist since 'stake' operation was successful and it wasn't a full unstake.", + ); assert_eq!(post_staker_info.period_number(), unstake_period); assert_eq!( post_staker_info.total_staked_amount(), @@ -989,7 +1019,7 @@ pub(crate) fn assert_claim_dapp_reward( assert_eq!( pre_reward_info.dapps.len(), post_reward_info.dapps.len() + 1, - "Entry must have been removed after successfull reward claim." + "Entry must have been removed after successful reward claim." ); } @@ -1463,3 +1493,17 @@ pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { second - first + 1 } + +/// Check whether the given account ledger's stake rewards have expired. +/// +/// `true` if expired, `false` otherwise. +pub(crate) fn is_account_ledger_expired( + ledger: &AccountLedgerFor, + current_period: PeriodNumber, +) -> bool { + let valid_threshold_period = DappStaking::oldest_claimable_period(current_period); + match ledger.staked_period() { + Some(staked_period) if staked_period < valid_threshold_period => true, + _ => false, + } +} diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 8839753b04..1a2a4cd4a0 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -332,7 +332,7 @@ fn set_dapp_reward_beneficiary_for_contract_is_ok() { // Update beneficiary assert!(IntegratedDApps::::get(&smart_contract) .unwrap() - .reward_destination + .reward_beneficiary .is_none()); assert_set_dapp_reward_beneficiary(owner, &smart_contract, Some(3)); assert_set_dapp_reward_beneficiary(owner, &smart_contract, Some(5)); diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 61c1740b5d..9d9a5d2e75 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -158,14 +158,14 @@ fn dapp_info_basic_checks() { owner, id: 7, state: DAppState::Registered, - reward_destination: None, + reward_beneficiary: 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); + dapp_info.reward_beneficiary = Some(beneficiary); assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); // Check if dApp is registered @@ -507,7 +507,7 @@ fn account_ledger_staked_era_period_works() { } #[test] -fn account_ledger_add_stake_amount_basic_example_works() { +fn account_ledger_add_stake_amount_basic_example_with_different_subperiods_works() { get_u32_type!(UnlockingDummy, 5); let mut acc_ledger = AccountLedger::::default(); @@ -554,6 +554,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { .period, period_1 ); + assert_eq!(acc_ledger.staked_future.unwrap().era, era_1 + 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(period_1), stake_amount); @@ -566,7 +567,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { .is_zero()); // Second scenario - stake some more, but to the next period type - let snapshot = acc_ledger.staked; + let snapshot = acc_ledger.staked_future.unwrap(); let period_info_2 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, @@ -583,6 +584,82 @@ fn account_ledger_add_stake_amount_basic_example_works() { acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period_1), 1 ); + + assert_eq!(acc_ledger.staked_future.unwrap().era, era_2 + 1); + assert_eq!(acc_ledger.staked_future.unwrap().voting, stake_amount); + assert_eq!(acc_ledger.staked_future.unwrap().build_and_earn, 1); + + assert_eq!(acc_ledger.staked, snapshot); +} + +#[test] +fn account_ledger_add_stake_amount_basic_example_with_same_subperiods_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // 1st scenario - stake some amount in first era of the `Build&Earn` subperiod, and ensure values are as expected. + let era_1 = 2; + let period_1 = 1; + let period_info = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + next_subperiod_start_era: 100, + }; + let lock_amount = 17; + let stake_amount = 11; + acc_ledger.add_lock_amount(lock_amount); + + assert!(acc_ledger + .add_stake_amount(stake_amount, era_1, period_info) + .is_ok()); + + assert!( + acc_ledger.staked.is_empty(), + "Current era must remain unchanged." + ); + assert_eq!(acc_ledger.staked_future.unwrap().period, period_1); + assert_eq!(acc_ledger.staked_future.unwrap().era, era_1 + 1); + assert_eq!( + acc_ledger.staked_future.unwrap().build_and_earn, + stake_amount + ); + assert!(acc_ledger.staked_future.unwrap().voting.is_zero()); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period_1), + stake_amount + ); + assert!(acc_ledger + .staked_amount_for_type(Subperiod::Voting, period_1) + .is_zero()); + + // 2nd scenario - stake again, in the same era + let snapshot = acc_ledger.staked; + assert!(acc_ledger.add_stake_amount(1, era_1, period_info).is_ok()); + assert_eq!(acc_ledger.staked, snapshot); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); + + // 2nd scenario - advance an era, and stake some more + let snapshot = acc_ledger.staked_future.unwrap(); + let era_2 = era_1 + 1; + assert!(acc_ledger.add_stake_amount(1, era_2, period_info).is_ok()); + + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 2); + assert!(acc_ledger + .staked_amount_for_type(Subperiod::Voting, period_1) + .is_zero(),); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period_1), + stake_amount + 2 + ); + assert_eq!(acc_ledger.staked_future.unwrap().period, period_1); + assert_eq!(acc_ledger.staked_future.unwrap().era, era_2 + 1); + assert_eq!( + acc_ledger.staked_future.unwrap().build_and_earn, + stake_amount + 2 + ); + assert!(acc_ledger.staked_future.unwrap().voting.is_zero()); + assert_eq!(acc_ledger.staked, snapshot); } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 671f5a0e97..1b88199a6b 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -114,8 +114,6 @@ pub enum AccountLedgerError { UnstakeAmountLargerThanStake, /// Nothing to claim. NothingToClaim, - /// Rewards have already been claimed - AlreadyClaimed, /// Attempt to crate the iterator failed due to incorrect data. InvalidIterator, } @@ -123,7 +121,7 @@ pub enum AccountLedgerError { /// Distinct subperiods in dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum Subperiod { - /// Subperiod during which the focus is on voting. + /// Subperiod during which the focus is on voting. No rewards are earned during this subperiod. Voting, /// Subperiod during which dApps and stakers earn rewards. BuildAndEarn, @@ -160,13 +158,13 @@ impl PeriodInfo { } } -/// Information describing relevant information for a finished period. +/// Struct with 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. + /// Total amount staked (remaining) from the voting subperiod. #[codec(compact)] pub total_vp_stake: Balance, /// Final era, inclusive, in which the period ended. @@ -241,10 +239,9 @@ impl ProtocolState { next_subperiod_start_era: EraNumber, next_era_start: BlockNumber, ) { - let period_number = if self.subperiod() == Subperiod::BuildAndEarn { - self.period_number().saturating_add(1) - } else { - self.period_number() + let period_number = match self.subperiod() { + Subperiod::Voting => self.period_number(), + Subperiod::BuildAndEarn => self.period_number().saturating_add(1), }; self.period_info = PeriodInfo { @@ -261,11 +258,11 @@ impl ProtocolState { pub enum DAppState { /// dApp is registered and active. Registered, - /// dApp has been unregistered in the contained era + /// dApp has been unregistered in the contained era. Unregistered(#[codec(compact)] EraNumber), } -/// General information about dApp. +/// General information about a dApp. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct DAppInfo { /// Owner of the dApp, default reward beneficiary. @@ -276,13 +273,13 @@ pub struct DAppInfo { /// Current state of the dApp. pub state: DAppState, // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`. - pub reward_destination: Option, + pub reward_beneficiary: Option, } impl DAppInfo { /// Reward destination account for this dApp. pub fn reward_beneficiary(&self) -> &AccountId { - match &self.reward_destination { + match &self.reward_beneficiary { Some(account_id) => account_id, None => &self.owner, } @@ -295,7 +292,7 @@ impl DAppInfo { } /// How much was unlocked in some block. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +#[derive(Encode, Decode, MaxEncodedLen, Clone, Default, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct UnlockingChunk { /// Amount undergoing the unlocking period. #[codec(compact)] @@ -305,15 +302,6 @@ pub struct UnlockingChunk { pub unlock_block: BlockNumber, } -impl Default for UnlockingChunk { - fn default() -> Self { - Self { - amount: Balance::zero(), - unlock_block: BlockNumber::zero(), - } - } -} - /// General info about an account's lock & stakes. /// /// ## Overview @@ -535,7 +523,7 @@ where .saturating_sub(self.staked_amount(active_period)) } - /// Amount that is staked, in respect to currently active period. + /// Amount that is staked, in respect to the currently active period. pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance { // First check the 'future' entry, afterwards check the 'first' entry match self.staked_future { @@ -569,13 +557,16 @@ where ) -> Result<(), AccountLedgerError> { if !self.staked.is_empty() { // In case entry for the current era exists, it must match the era exactly. + // No other scenario is possible since stake/unstake is not allowed without claiming rewards first. if self.staked.era != current_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. + // In case only the 'future' entry exists, then the future era must either be the current or the next era. + // 'Next era' covers the simple scenario where stake is only valid from the next era. + // 'Current era' covers the scenario where stake was made in previous era, and we've moved to the next era. } else if let Some(stake_amount) = self.staked_future { if stake_amount.era != current_era.saturating_add(1) && stake_amount.era != current_era { @@ -618,7 +609,13 @@ where // Update existing entry if it exists, otherwise create it. match self.staked_future.as_mut() { Some(stake_amount) => { + // In case future entry exists, check if it should be moved over to the 'current' entry. + if stake_amount.era == current_era { + self.staked = *stake_amount; + } + stake_amount.add(amount, current_period_info.subperiod); + stake_amount.era = current_era.saturating_add(1); } None => { let mut stake_amount = self.staked; @@ -771,7 +768,7 @@ where /// Helper internal struct for iterating over `(era, stake amount)` pairs. /// -/// Due to how `AccountLedger` is implemented, few scenarios are possible when claming rewards: +/// Due to how `AccountLedger` is implemented, few scenarios are possible when claiming rewards: /// /// 1. `staked` has some amount, `staked_future` is `None` /// * `maybe_first` is `None`, span describes the entire range @@ -785,7 +782,7 @@ pub struct EraStakePairIter { maybe_first: Option<(EraNumber, Balance)>, /// Starting era of the span. start_era: EraNumber, - /// Ending era of the span. + /// Ending era of the span, inclusive. end_era: EraNumber, /// Staked amount in the span. amount: Balance, @@ -841,10 +838,10 @@ impl Iterator for EraStakePairIter { /// 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. + /// Amount of staked funds accounting for the voting subperiod. #[codec(compact)] pub voting: Balance, - /// Amount of staked funds accounting for the build&earn period. + /// Amount of staked funds accounting for the build&earn subperiod. #[codec(compact)] pub build_and_earn: Balance, /// Era to which this stake amount refers to. @@ -884,10 +881,10 @@ impl StakeAmount { /// Unstake the specified `amount` for the specified `subperiod`. /// - /// In case subperiod is `Voting`, the amount is subtracted from the voting period. + /// In case subperiod is `Voting`, the amount is subtracted from the voting subperiod. /// /// 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. + /// build&earn amount, and any rollover is subtracted from the voting subperiod. pub fn subtract(&mut self, amount: Balance, subperiod: Subperiod) { match subperiod { Subperiod::Voting => self.voting.saturating_reduce(amount), @@ -909,7 +906,7 @@ impl StakeAmount { } } -/// Info about current era, including the rewards, how much is locked, unlocking, etc. +/// Info about an era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { /// How much balance is locked in dApp staking. @@ -969,7 +966,7 @@ impl EraInfo { self.next_stake_amount.total() } - /// Staked amount of specifeid `type` in the next era. + /// Staked amount of specified `type` in the next era. pub fn staked_amount_next_era(&self, subperiod: Subperiod) -> Balance { self.next_stake_amount.for_type(subperiod) } @@ -980,7 +977,7 @@ impl EraInfo { /// `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) { match next_subperiod { - // If next era marks start of new voting period period, it means we're entering a new period + // If next era marks start of new voting subperiod period, it means we're entering a new period Some(Subperiod::Voting) => { for stake_amount in [&mut self.current_stake_amount, &mut self.next_stake_amount] { stake_amount.voting = Zero::zero(); @@ -999,7 +996,7 @@ impl EraInfo { /// Information about how much a particular staker staked on a particular smart contract. /// -/// Keeps track of amount staked in the 'voting period', as well as 'build&earn period'. +/// Keeps track of amount staked in the 'voting subperiod', as well as 'build&earn subperiod'. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct SingularStakingInfo { /// Staked amount @@ -1023,7 +1020,7 @@ impl SingularStakingInfo { era: 0, period, }, - // Loyalty staking is only possible if stake is first made during the voting period. + // Loyalty staking is only possible if stake is first made during the voting subperiod. loyal_staker: subperiod == Subperiod::Voting, } } @@ -1037,10 +1034,10 @@ impl SingularStakingInfo { /// Unstakes some of the specified amount from the contract. /// - /// In case the `amount` being unstaked is larger than the amount staked in the `voting period`, - /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. + /// In case the `amount` being unstaked is larger than the amount staked in the `Voting` subperiod, + /// and `Voting` subperiod 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. + /// Returns the amount that was unstaked from the `Voting` subperiod stake, and from the `Build&Earn` subperiod stake. pub fn unstake( &mut self, amount: Balance, @@ -1076,7 +1073,7 @@ impl SingularStakingInfo { self.staked.for_type(subperiod) } - /// If `true` staker has staked during voting period and has never reduced their sta + /// If `true` staker has staked during voting subperiod and has never reduced their sta pub fn is_loyal(&self) -> bool { self.loyal_staker } @@ -1194,7 +1191,7 @@ impl ContractStakeAmount { stake_amount.add(amount, period_info.subperiod); return; } - // Future entry has older era, but periods match so overwrite the 'current' entry with it + // Future entry has an 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; } @@ -1247,7 +1244,7 @@ impl ContractStakeAmount { stake_amount.subtract(amount, period_info.subperiod); } - // Conevnience cleanup + // Convenience cleanup if self.staked.is_empty() { self.staked = Default::default(); } @@ -1338,7 +1335,7 @@ where } /// 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. + /// If span is not empty, the provided `era` must be exactly one era after the last one in the span. pub fn push( &mut self, era: EraNumber, @@ -1534,7 +1531,7 @@ impl> TiersConfiguration { 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. + // All vector 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. @@ -1715,7 +1712,7 @@ impl, NT: Get> DAppTierRewards { pub enum DAppTierError { /// Specified dApp Id doesn't exist in any tier. NoDAppInTiers, - /// Internal, unexpected error occured. + /// Internal, unexpected error occurred. InternalError, } diff --git a/pallets/inflation/src/lib.rs b/pallets/inflation/src/lib.rs index b643fe5e32..8d278e1c00 100644 --- a/pallets/inflation/src/lib.rs +++ b/pallets/inflation/src/lib.rs @@ -69,9 +69,9 @@ //! //! ## Rewards //! -//! ### Staker & Treasury Rewards +//! ### Collator & Treasury Rewards //! -//! These are paid out at the begininng of each block & are fixed amounts. +//! These are paid out at the beginning of each block & are fixed amounts. //! //! ### Staker Rewards //! @@ -177,7 +177,7 @@ pub mod pallet { InvalidInflationParameters, } - /// Active inflation configuration parameteres. + /// Active inflation configuration parameters. /// They describe current rewards, when inflation needs to be recalculated, etc. #[pallet::storage] #[pallet::whitelist_storage] @@ -219,7 +219,7 @@ pub mod pallet { T::WeightInfo::hook_without_recalculation() }; - // Benchmarks won't acount for whitelisted storage access so this needs to be added manually. + // Benchmarks won't account for the whitelisted storage access so this needs to be added manually. // // ActiveInflationConfig - 1 DB read let whitelisted_weight = ::DbWeight::get().reads(1); @@ -257,7 +257,7 @@ pub mod pallet { /// /// Must be called by `root` origin. /// - /// Purpose of the call is testing & handling unforseen circumstances. + /// Purpose of the call is testing & handling unforeseen circumstances. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::force_set_inflation_params())] pub fn force_set_inflation_params( @@ -279,7 +279,7 @@ pub mod pallet { /// /// Must be called by `root` origin. /// - /// Purpose of the call is testing & handling unforseen circumstances. + /// Purpose of the call is testing & handling unforeseen circumstances. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::force_inflation_recalculation())] pub fn force_inflation_recalculation(origin: OriginFor) -> DispatchResult { @@ -299,7 +299,7 @@ pub mod pallet { /// /// Must be called by `root` origin. /// - /// Purpose of the call is testing & handling unforseen circumstances. + /// Purpose of the call is testing & handling unforeseen circumstances. /// /// **NOTE:** and a TODO, remove this before deploying on mainnet. #[pallet::call_index(2)] @@ -368,23 +368,13 @@ pub mod pallet { // 3.0 Convert all 'per cycle' values to the correct type (Balance). // Also include a safety check that none of the values is zero since this would cause a division by zero. // The configuration & integration tests must ensure this never happens, so the following code is just an additional safety measure. - let blocks_per_cycle = match T::CycleConfiguration::blocks_per_cycle() { - 0 => Balance::MAX, - blocks_per_cycle => Balance::from(blocks_per_cycle), - }; - + let blocks_per_cycle = Balance::from(T::CycleConfiguration::blocks_per_cycle().max(1)); let build_and_earn_eras_per_cycle = - match T::CycleConfiguration::build_and_earn_eras_per_cycle() { - 0 => Balance::MAX, - build_and_earn_eras_per_cycle => Balance::from(build_and_earn_eras_per_cycle), - }; - - let periods_per_cycle = match T::CycleConfiguration::periods_per_cycle() { - 0 => Balance::MAX, - periods_per_cycle => Balance::from(periods_per_cycle), - }; + Balance::from(T::CycleConfiguration::build_and_earn_eras_per_cycle().max(1)); + let periods_per_cycle = + Balance::from(T::CycleConfiguration::periods_per_cycle().max(1)); - // 3.1. Collator & Treausry rewards per block + // 3.1. Collator & Treasury rewards per block let collator_reward_per_block = collators_emission / blocks_per_cycle; let treasury_reward_per_block = treasury_emission / blocks_per_cycle; @@ -500,7 +490,7 @@ pub struct InflationConfiguration { /// Base staker reward pool per era - this is always provided to stakers, regardless of the total value staked. #[codec(compact)] pub base_staker_reward_pool_per_era: Balance, - /// Adjustabke staker rewards, based on the total value staked. + /// Adjustable staker rewards, based on the total value staked. /// This is provided to the stakers according to formula: 'pool * min(1, total_staked / ideal_staked)'. #[codec(compact)] pub adjustable_staker_reward_pool_per_era: Balance, @@ -612,7 +602,7 @@ impl> OnRuntimeUpgrade for PalletInflatio let inflation_params = P::get(); InflationParams::::put(inflation_params.clone()); - // 2. Calculation inflation config, set it & depossit event + // 2. Calculation inflation config, set it & deposit event let now = frame_system::Pallet::::block_number(); let config = Pallet::::recalculate_inflation(now); ActiveInflationConfig::::put(config.clone()); diff --git a/pallets/inflation/src/tests.rs b/pallets/inflation/src/tests.rs index 518aa369c4..db57a25cfb 100644 --- a/pallets/inflation/src/tests.rs +++ b/pallets/inflation/src/tests.rs @@ -37,7 +37,7 @@ fn force_set_inflation_params_work() { ExternalityBuilder::build().execute_with(|| { let mut new_params = InflationParams::::get(); new_params.max_inflation_rate = Perquintill::from_percent(20); - assert!(new_params != InflationParams::::get(), "Sanity check"); + assert_ne!(new_params, InflationParams::::get(), "Sanity check"); // Execute call, ensure it works assert_ok!(Inflation::force_set_inflation_params( @@ -118,8 +118,8 @@ fn force_inflation_recalculation_work() { )); let new_config = ActiveInflationConfig::::get(); - assert!( - old_config != new_config, + assert_ne!( + old_config, new_config, "Config should change, otherwise test doesn't make sense." ); @@ -163,8 +163,8 @@ fn inflation_recalculation_occurs_when_exepcted() { Inflation::on_finalize(init_config.recalculation_block - 1); let new_config = ActiveInflationConfig::::get(); - assert!( - new_config != init_config, + assert_ne!( + new_config, init_config, "Recalculation must happen at this point." ); System::assert_last_event(Event::NewInflationConfiguration { config: new_config }.into()); diff --git a/precompiles/dapp-staking-v3/src/test/mod.rs b/precompiles/dapp-staking-v3/src/test/mod.rs index a33eb22954..5bc195b3ce 100644 --- a/precompiles/dapp-staking-v3/src/test/mod.rs +++ b/precompiles/dapp-staking-v3/src/test/mod.rs @@ -17,6 +17,6 @@ // along with Astar. If not, see . mod mock; -mod tests_v1; mod tests_v2; +mod tests_v3; mod types; diff --git a/precompiles/dapp-staking-v3/src/test/tests_v1.rs b/precompiles/dapp-staking-v3/src/test/tests_v1.rs deleted file mode 100644 index 8d93b56198..0000000000 --- a/precompiles/dapp-staking-v3/src/test/tests_v1.rs +++ /dev/null @@ -1,867 +0,0 @@ -// 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 . - -extern crate alloc; -use crate::{test::mock::*, *}; -use frame_support::assert_ok; -use frame_system::RawOrigin; -use precompile_utils::testing::*; -use sp_core::H160; -use sp_runtime::traits::Zero; - -use assert_matches::assert_matches; - -use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber, EraRewards}; - -#[test] -fn read_current_era_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - precompiles() - .prepare_test( - Alice, - precompile_address(), - PrecompileCall::read_current_era {}, - ) - .expect_no_logs() - .execute_returns(ActiveProtocolState::::get().era); - - // advance a few eras, check value again - advance_to_era(7); - precompiles() - .prepare_test( - Alice, - precompile_address(), - PrecompileCall::read_current_era {}, - ) - .expect_no_logs() - .execute_returns(ActiveProtocolState::::get().era); - }); -} - -#[test] -fn read_unbonding_period_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - let unlocking_period_in_eras: EraNumber = - ::UnlockingPeriod::get(); - - precompiles() - .prepare_test( - Alice, - precompile_address(), - PrecompileCall::read_unbonding_period {}, - ) - .expect_no_logs() - .execute_returns(unlocking_period_in_eras); - }); -} - -#[test] -fn read_era_reward_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Check historic era for rewards - let era = 3; - advance_to_era(era + 1); - - let span_index = DAppStaking::::era_reward_span_index(era); - - let era_rewards_span = EraRewards::::get(span_index).expect("Entry must exist."); - let expected_reward = era_rewards_span - .get(era) - .map(|r| r.staker_reward_pool + r.dapp_reward_pool) - .expect("It's history era so it must exist."); - assert!(expected_reward > 0, "Sanity check."); - - precompiles() - .prepare_test( - Alice, - precompile_address(), - PrecompileCall::read_era_reward { era }, - ) - .expect_no_logs() - .execute_returns(expected_reward); - - // Check current era for rewards, must be zero - precompiles() - .prepare_test( - Alice, - precompile_address(), - PrecompileCall::read_era_reward { era: era + 1 }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - }); -} - -#[test] -fn read_era_staked_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - let anchor_era = ActiveProtocolState::::get().era; - - // 1. Current era stake must be zero, since stake is only valid from the next era. - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_era_staked { era: anchor_era }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_era_staked { - era: anchor_era + 1, - }, - ) - .expect_no_logs() - .execute_returns(amount); - - // 2. Advance to next era, and check next era after the anchor. - advance_to_era(anchor_era + 5); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_era_staked { - era: anchor_era + 1, - }, - ) - .expect_no_logs() - .execute_returns(amount); - - // 3. Check era after the next one, must throw an error. - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_era_staked { - era: ActiveProtocolState::::get().era + 2, - }, - ) - .expect_no_logs() - .execute_reverts(|output| output == b"Era is in the future"); - }); -} - -#[test] -fn read_staked_amount_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - let staker_h160 = ALICE; - let dynamic_addresses = into_dynamic_addresses(staker_h160); - - // 1. Sanity checks - must be zero before anything is staked. - for staker in &dynamic_addresses { - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_staked_amount { - staker: staker.clone(), - }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - } - - // 2. Stake some amount and check again - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - for staker in &dynamic_addresses { - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_staked_amount { - staker: staker.clone(), - }, - ) - .expect_no_logs() - .execute_returns(amount); - } - - // 3. Advance into next period, it should be reset back to zero - advance_to_next_period(); - for staker in &dynamic_addresses { - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_staked_amount { - staker: staker.clone(), - }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - } - }); -} - -#[test] -fn read_staked_amount_on_contract_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let dynamic_addresses = into_dynamic_addresses(staker_h160); - - // 1. Sanity checks - must be zero before anything is staked. - for staker in &dynamic_addresses { - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_staked_amount_on_contract { - contract_h160: smart_contract_address.into(), - staker: staker.clone(), - }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - } - - // 2. Stake some amount and check again - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - for staker in &dynamic_addresses { - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_staked_amount_on_contract { - contract_h160: smart_contract_address.into(), - staker: staker.clone(), - }, - ) - .expect_no_logs() - .execute_returns(amount); - } - - // 3. Advance into next period, it should be reset back to zero - advance_to_next_period(); - for staker in &dynamic_addresses { - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_staked_amount_on_contract { - contract_h160: smart_contract_address.into(), - staker: staker.clone(), - }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - } - }); -} - -#[test] -fn read_contract_stake_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - - // 1. Sanity checks - must be zero before anything is staked. - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_contract_stake { - contract_h160: smart_contract_address.into(), - }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - - // 2. Stake some amount and check again - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_contract_stake { - contract_h160: smart_contract_address.into(), - }, - ) - .expect_no_logs() - .execute_returns(amount); - - // 3. Advance into next period, it should be reset back to zero - advance_to_next_period(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::read_contract_stake { - contract_h160: smart_contract_address.into(), - }, - ) - .expect_no_logs() - .execute_returns(Balance::zero()); - }); -} - -#[test] -fn register_is_unsupported() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - precompiles() - .prepare_test( - ALICE, - precompile_address(), - PrecompileCall::register { - _address: Default::default(), - }, - ) - .expect_no_logs() - .execute_reverts(|output| output == b"register via evm precompile is not allowed"); - }); -} - -#[test] -fn set_reward_destination_is_unsupported() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - precompiles() - .prepare_test( - ALICE, - precompile_address(), - PrecompileCall::set_reward_destination { _destination: 0 }, - ) - .expect_no_logs() - .execute_reverts(|output| { - output == b"Setting reward destination is no longer supported." - }); - }); -} - -#[test] -fn bond_and_stake_with_two_calls_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - assert_ok!(DappStaking::register( - RawOrigin::Root.into(), - AddressMapper::into_account_id(staker_h160), - smart_contract.clone() - )); - - // Lock some amount, but not enough to cover the `bond_and_stake` call. - let pre_lock_amount = 500; - let stake_amount = 1_000_000; - assert_ok!(DappStaking::lock( - RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), - pre_lock_amount, - )); - - // Execute legacy call, expect missing funds to be locked. - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::bond_and_stake { - contract_h160: smart_contract_address.into(), - amount: stake_amount, - }, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 2); - let additional_lock_amount = stake_amount - pre_lock_amount; - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Locked { - amount, - .. - } if amount == additional_lock_amount - ); - assert_matches!( - events[1].clone(), - pallet_dapp_staking_v3::Event::Stake { - smart_contract, - amount, - .. - } if smart_contract == smart_contract && amount == stake_amount - ); - }); -} - -#[test] -fn bond_and_stake_with_single_call_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - assert_ok!(DappStaking::register( - RawOrigin::Root.into(), - AddressMapper::into_account_id(staker_h160), - smart_contract.clone() - )); - - // Lock enough amount to cover `bond_and_stake` call. - let amount = 3000; - assert_ok!(DappStaking::lock( - RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), - amount, - )); - - // Execute legacy call, expect only single stake to be executed. - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::bond_and_stake { - contract_h160: smart_contract_address.into(), - amount, - }, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Stake { - smart_contract, - amount, - .. - } if smart_contract == smart_contract && amount == amount - ); - }); -} - -#[test] -fn unbond_and_unstake_with_two_calls_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - // Execute legacy call, expect funds to first unstaked, and then unlocked - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::unbond_and_unstake { - contract_h160: smart_contract_address.into(), - amount, - }, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 2); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Unstake { - smart_contract, - amount, - .. - }if smart_contract == smart_contract && amount == amount - ); - assert_matches!( - events[1].clone(), - pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount - ); - }); -} - -#[test] -fn unbond_and_unstake_with_single_calls_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - // Unstake the entire amount, so only unlock call is expected. - assert_ok!(DappStaking::unstake( - RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), - smart_contract.clone(), - amount, - )); - - // Execute legacy call, expect funds to be unlocked - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::unbond_and_unstake { - contract_h160: smart_contract_address.into(), - amount, - }, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount - ); - }); -} - -#[test] -fn withdraw_unbonded_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let staker_native = AddressMapper::into_account_id(staker_h160); - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - // Unlock some amount - assert_ok!(DappStaking::unstake( - RawOrigin::Signed(staker_native.clone()).into(), - smart_contract.clone(), - amount, - )); - let unlock_amount = amount / 7; - assert_ok!(DappStaking::unlock( - RawOrigin::Signed(staker_native.clone()).into(), - unlock_amount, - )); - - // Advance enough into time so unlocking chunk can be claimed - let unlock_block = Ledger::::get(&staker_native).unlocking[0].unlock_block; - run_to_block(unlock_block); - - // Execute legacy call, expect unlocked funds to be claimed back - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::withdraw_unbonded {}, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::ClaimedUnlocked { - amount, - .. - } if amount == unlock_amount - ); - }); -} - -#[test] -fn claim_dapp_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - // Advance enough eras so we can claim dApp reward - advance_to_era(3); - let claim_era = 2; - - // Execute legacy call, expect dApp rewards to be claimed - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::claim_dapp { - contract_h160: smart_contract_address.into(), - era: claim_era, - }, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::DAppReward { - era, - smart_contract, - .. - } if era as u128 == claim_era && smart_contract == smart_contract - ); - }); -} - -#[test] -fn claim_staker_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - // Advance enough eras so we can claim dApp reward - let target_era = 5; - advance_to_era(target_era); - let number_of_claims = (2..target_era).count(); - - // Execute legacy call, expect dApp rewards to be claimed - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::claim_staker { - _contract_h160: smart_contract_address.into(), - }, - ) - .expect_no_logs() - .execute_returns(true); - - // We expect multiple reward to be claimed - let events = dapp_staking_events(); - assert_eq!(events.len(), number_of_claims as usize); - for era in 2..target_era { - assert_matches!( - events[era as usize - 2].clone(), - pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era - ); - } - }); -} - -#[test] -fn withdraw_from_unregistered_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register a dApp for staking - let staker_h160 = ALICE; - let smart_contract_address = H160::repeat_byte(0xFA); - let smart_contract = - ::SmartContract::evm(smart_contract_address); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract.clone(), amount); - - // Unregister the dApp - assert_ok!(DappStaking::unregister( - RawOrigin::Root.into(), - smart_contract.clone() - )); - - // Execute legacy call, expect funds to be unstaked & withdrawn - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::withdraw_from_unregistered { - contract_h160: smart_contract_address.into(), - }, - ) - .expect_no_logs() - .execute_returns(true); - - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { - smart_contract, - amount, - .. - } if smart_contract == smart_contract && amount == amount - ); - }); -} - -#[test] -fn nomination_transfer_is_ok() { - ExternalityBuilder::build().execute_with(|| { - initialize(); - - // Register the first dApp, and stke on it. - let staker_h160 = ALICE; - let staker_native = AddressMapper::into_account_id(staker_h160); - let smart_contract_address_1 = H160::repeat_byte(0xFA); - let smart_contract_1 = - ::SmartContract::evm(smart_contract_address_1); - let amount = 1_000_000_000_000; - register_and_stake(staker_h160, smart_contract_1.clone(), amount); - - // Register the second dApp. - let smart_contract_address_2 = H160::repeat_byte(0xBF); - let smart_contract_2 = - ::SmartContract::evm(smart_contract_address_2); - assert_ok!(DappStaking::register( - RawOrigin::Root.into(), - staker_native.clone(), - smart_contract_2.clone() - )); - - // 1st scenario - transfer enough amount from the first to second dApp to cover the stake, - // but not enough for full unstake. - let minimum_stake_amount: Balance = - ::MinimumStakeAmount::get(); - - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::nomination_transfer { - origin_contract_h160: smart_contract_address_1.into(), - amount: minimum_stake_amount, - target_contract_h160: smart_contract_address_2.into(), - }, - ) - .expect_no_logs() - .execute_returns(true); - - // We expect the same amount to be staked on the second contract - let events = dapp_staking_events(); - assert_eq!(events.len(), 2); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Unstake { - smart_contract, - amount, - .. - } if smart_contract == smart_contract_1 && amount == minimum_stake_amount - ); - assert_matches!( - events[1].clone(), - pallet_dapp_staking_v3::Event::Stake { - smart_contract, - amount, - .. - } if smart_contract == smart_contract_2 && amount == minimum_stake_amount - ); - - // 2nd scenario - transfer almost the entire amount from the first to second dApp. - // The amount is large enough to trigger full unstake of the first contract. - let unstake_amount = amount - minimum_stake_amount - 1; - let expected_stake_unstake_amount = amount - minimum_stake_amount; - - System::reset_events(); - precompiles() - .prepare_test( - staker_h160, - precompile_address(), - PrecompileCall::nomination_transfer { - origin_contract_h160: smart_contract_address_1.into(), - amount: unstake_amount, - target_contract_h160: smart_contract_address_2.into(), - }, - ) - .expect_no_logs() - .execute_returns(true); - - // We expect the same amount to be staked on the second contract - let events = dapp_staking_events(); - assert_eq!(events.len(), 2); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Unstake { - smart_contract, - amount, - .. - } if smart_contract == smart_contract_1 && amount == expected_stake_unstake_amount - ); - assert_matches!( - events[1].clone(), - pallet_dapp_staking_v3::Event::Stake { - smart_contract, - amount, - .. - } if smart_contract == smart_contract_2 && amount == expected_stake_unstake_amount - ); - }); -} diff --git a/precompiles/dapp-staking-v3/src/test/tests_v2.rs b/precompiles/dapp-staking-v3/src/test/tests_v2.rs index 5977417b5e..8d93b56198 100644 --- a/precompiles/dapp-staking-v3/src/test/tests_v2.rs +++ b/precompiles/dapp-staking-v3/src/test/tests_v2.rs @@ -22,205 +22,455 @@ use frame_support::assert_ok; use frame_system::RawOrigin; use precompile_utils::testing::*; use sp_core::H160; +use sp_runtime::traits::Zero; use assert_matches::assert_matches; -use astar_primitives::{dapp_staking::CycleConfiguration, BlockNumber}; -use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber}; +use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber, EraRewards}; #[test] -fn protocol_state_is_ok() { +fn read_current_era_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Prepare some mixed state in the future so not all entries are 'zero' - advance_to_next_period(); - advance_to_next_era(); - - let state = ActiveProtocolState::::get(); - - let expected_outcome = PrecompileProtocolState { - era: state.era.into(), - period: state.period_number().into(), - subperiod: subperiod_id(&state.subperiod()), - }; + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_current_era {}, + ) + .expect_no_logs() + .execute_returns(ActiveProtocolState::::get().era); + // advance a few eras, check value again + advance_to_era(7); precompiles() .prepare_test( Alice, precompile_address(), - PrecompileCall::protocol_state {}, + PrecompileCall::read_current_era {}, ) .expect_no_logs() - .execute_returns(expected_outcome); + .execute_returns(ActiveProtocolState::::get().era); }); } #[test] -fn unlocking_period_is_ok() { +fn read_unbonding_period_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); let unlocking_period_in_eras: EraNumber = ::UnlockingPeriod::get(); - let era_length: BlockNumber = - ::CycleConfiguration::blocks_per_era(); - let expected_outcome = era_length * unlocking_period_in_eras; + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_unbonding_period {}, + ) + .expect_no_logs() + .execute_returns(unlocking_period_in_eras); + }); +} + +#[test] +fn read_era_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Check historic era for rewards + let era = 3; + advance_to_era(era + 1); + + let span_index = DAppStaking::::era_reward_span_index(era); + + let era_rewards_span = EraRewards::::get(span_index).expect("Entry must exist."); + let expected_reward = era_rewards_span + .get(era) + .map(|r| r.staker_reward_pool + r.dapp_reward_pool) + .expect("It's history era so it must exist."); + assert!(expected_reward > 0, "Sanity check."); precompiles() .prepare_test( Alice, precompile_address(), - PrecompileCall::unlocking_period {}, + PrecompileCall::read_era_reward { era }, ) .expect_no_logs() - .execute_returns(expected_outcome); + .execute_returns(expected_reward); + + // Check current era for rewards, must be zero + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_era_reward { era: era + 1 }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); }); } #[test] -fn lock_is_ok() { +fn read_era_staked_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Lock some amount and verify event - let amount = 1234; - System::reset_events(); + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + let anchor_era = ActiveProtocolState::::get().era; + + // 1. Current era stake must be zero, since stake is only valid from the next era. precompiles() - .prepare_test(ALICE, precompile_address(), PrecompileCall::lock { amount }) + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { era: anchor_era }, + ) .expect_no_logs() - .execute_returns(true); + .execute_returns(Balance::zero()); - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Locked { - amount, - .. - } if amount == amount - ); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: anchor_era + 1, + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 2. Advance to next era, and check next era after the anchor. + advance_to_era(anchor_era + 5); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: anchor_era + 1, + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 3. Check era after the next one, must throw an error. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: ActiveProtocolState::::get().era + 2, + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Era is in the future"); }); } #[test] -fn unlock_is_ok() { +fn read_staked_amount_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - let lock_amount = 1234; - assert_ok!(DappStaking::lock( - RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), - lock_amount, - )); + let staker_h160 = ALICE; + let dynamic_addresses = into_dynamic_addresses(staker_h160); + + // 1. Sanity checks - must be zero before anything is staked. + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + + // 2. Stake some amount and check again + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + } + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + }); +} + +#[test] +fn read_staked_amount_on_contract_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let dynamic_addresses = into_dynamic_addresses(staker_h160); + + // 1. Sanity checks - must be zero before anything is staked. + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + + // 2. Stake some amount and check again + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + } + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + }); +} + +#[test] +fn read_contract_stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + + // 1. Sanity checks - must be zero before anything is staked. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + + // 2. Stake some amount and check again + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + }); +} + +#[test] +fn register_is_unsupported() { + ExternalityBuilder::build().execute_with(|| { + initialize(); - // Unlock some amount and verify event - System::reset_events(); - let unlock_amount = 1234 / 7; precompiles() .prepare_test( ALICE, precompile_address(), - PrecompileCall::unlock { - amount: unlock_amount, + PrecompileCall::register { + _address: Default::default(), }, ) .expect_no_logs() - .execute_returns(true); + .execute_reverts(|output| output == b"register via evm precompile is not allowed"); + }); +} - let events = dapp_staking_events(); - assert_eq!(events.len(), 1); - assert_matches!( - events[0].clone(), - pallet_dapp_staking_v3::Event::Unlocking { - amount, - .. - } if amount == unlock_amount - ); +#[test] +fn set_reward_destination_is_unsupported() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::set_reward_destination { _destination: 0 }, + ) + .expect_no_logs() + .execute_reverts(|output| { + output == b"Setting reward destination is no longer supported." + }); }); } #[test] -fn claim_unlocked_is_ok() { +fn bond_and_stake_with_two_calls_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Lock/unlock some amount to create unlocking chunk - let amount = 1234; - assert_ok!(DappStaking::lock( - RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), - amount, - )); - assert_ok!(DappStaking::unlock( - RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), - amount, + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() )); - // Advance enough into time so unlocking chunk can be claimed - let unlock_block = - Ledger::::get(&AddressMapper::into_account_id(ALICE)).unlocking[0].unlock_block; - run_to_block(unlock_block); + // Lock some amount, but not enough to cover the `bond_and_stake` call. + let pre_lock_amount = 500; + let stake_amount = 1_000_000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + pre_lock_amount, + )); - // Claim unlocked chunk and verify event + // Execute legacy call, expect missing funds to be locked. System::reset_events(); precompiles() .prepare_test( - ALICE, + staker_h160, precompile_address(), - PrecompileCall::claim_unlocked {}, + PrecompileCall::bond_and_stake { + contract_h160: smart_contract_address.into(), + amount: stake_amount, + }, ) .expect_no_logs() .execute_returns(true); let events = dapp_staking_events(); - assert_eq!(events.len(), 1); + assert_eq!(events.len(), 2); + let additional_lock_amount = stake_amount - pre_lock_amount; assert_matches!( events[0].clone(), - pallet_dapp_staking_v3::Event::ClaimedUnlocked { + pallet_dapp_staking_v3::Event::Locked { amount, .. - } if amount == amount + } if amount == additional_lock_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == stake_amount ); }); } #[test] -fn stake_is_ok() { +fn bond_and_stake_with_single_call_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); // Register a dApp for staking let staker_h160 = ALICE; - let smart_contract_h160 = H160::repeat_byte(0xFA); + let smart_contract_address = H160::repeat_byte(0xFA); let smart_contract = - ::SmartContract::evm(smart_contract_h160); + ::SmartContract::evm(smart_contract_address); assert_ok!(DappStaking::register( RawOrigin::Root.into(), AddressMapper::into_account_id(staker_h160), smart_contract.clone() )); - // Lock some amount which will be used for staking - let amount = 2000; + // Lock enough amount to cover `bond_and_stake` call. + let amount = 3000; assert_ok!(DappStaking::lock( RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), amount, )); - let smart_contract_v2 = SmartContractV2 { - contract_type: SmartContractTypes::Evm, - address: smart_contract_h160.as_bytes().try_into().unwrap(), - }; - - // Stake some amount and verify event + // Execute legacy call, expect only single stake to be executed. System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::stake { - smart_contract: smart_contract_v2, + PrecompileCall::bond_and_stake { + contract_h160: smart_contract_address.into(), amount, }, ) @@ -241,47 +491,26 @@ fn stake_is_ok() { } #[test] -fn unstake_is_ok() { +fn unbond_and_unstake_with_two_calls_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); // Register a dApp for staking let staker_h160 = ALICE; - let smart_contract_address = [0xAF; 32]; - let smart_contract = ::SmartContract::wasm( - smart_contract_address.into(), - ); - assert_ok!(DappStaking::register( - RawOrigin::Root.into(), - AddressMapper::into_account_id(staker_h160), - smart_contract.clone() - )); - - // Lock & stake some amount - let amount = 2000; - assert_ok!(DappStaking::lock( - RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), - amount, - )); - assert_ok!(DappStaking::stake( - RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), - smart_contract.clone(), - amount, - )); - - let smart_contract_v2 = SmartContractV2 { - contract_type: SmartContractTypes::Wasm, - address: smart_contract_address.into(), - }; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); - // Unstake some amount and verify event + // Execute legacy call, expect funds to first unstaked, and then unlocked System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::unstake { - smart_contract: smart_contract_v2, + PrecompileCall::unbond_and_unstake { + contract_h160: smart_contract_address.into(), amount, }, ) @@ -289,91 +518,102 @@ fn unstake_is_ok() { .execute_returns(true); let events = dapp_staking_events(); - assert_eq!(events.len(), 1); + assert_eq!(events.len(), 2); assert_matches!( events[0].clone(), pallet_dapp_staking_v3::Event::Unstake { smart_contract, amount, .. - } if smart_contract == smart_contract && amount == amount + }if smart_contract == smart_contract && amount == amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount ); }); } #[test] -fn claim_staker_rewards_is_ok() { +fn unbond_and_unstake_with_single_calls_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Register a dApp and stake on it + // Register a dApp for staking let staker_h160 = ALICE; - let smart_contract_address = [0xAF; 32]; - let smart_contract = ::SmartContract::wasm( - smart_contract_address.into(), - ); - let amount = 1234; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; register_and_stake(staker_h160, smart_contract.clone(), amount); - // Advance a few eras so we can claim a few rewards - let target_era = 7; - advance_to_era(target_era); - let number_of_claims = (2..target_era).count(); + // Unstake the entire amount, so only unlock call is expected. + assert_ok!(DappStaking::unstake( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + smart_contract.clone(), + amount, + )); - // Claim staker rewards and verify events + // Execute legacy call, expect funds to be unlocked System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::claim_staker_rewards {}, + PrecompileCall::unbond_and_unstake { + contract_h160: smart_contract_address.into(), + amount, + }, ) .expect_no_logs() .execute_returns(true); - // We expect multiple reward to be claimed let events = dapp_staking_events(); - assert_eq!(events.len(), number_of_claims as usize); - for era in 2..target_era { - assert_matches!( - events[era as usize - 2].clone(), - pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era - ); - } + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount + ); }); } #[test] -fn claim_bonus_reward_is_ok() { +fn withdraw_unbonded_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Register a dApp and stake on it, loyally + // Register a dApp for staking let staker_h160 = ALICE; - let smart_contract_address = [0xAF; 32]; - let smart_contract = ::SmartContract::wasm( - smart_contract_address.into(), - ); - let amount = 1234; + let staker_native = AddressMapper::into_account_id(staker_h160); + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; register_and_stake(staker_h160, smart_contract.clone(), amount); - // Advance to the next period - advance_to_next_period(); + // Unlock some amount + assert_ok!(DappStaking::unstake( + RawOrigin::Signed(staker_native.clone()).into(), + smart_contract.clone(), + amount, + )); + let unlock_amount = amount / 7; + assert_ok!(DappStaking::unlock( + RawOrigin::Signed(staker_native.clone()).into(), + unlock_amount, + )); - let smart_contract_v2 = SmartContractV2 { - contract_type: SmartContractTypes::Wasm, - address: smart_contract_address.into(), - }; + // Advance enough into time so unlocking chunk can be claimed + let unlock_block = Ledger::::get(&staker_native).unlocking[0].unlock_block; + run_to_block(unlock_block); - // Claim bonus reward and verify event + // Execute legacy call, expect unlocked funds to be claimed back System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::claim_bonus_reward { - smart_contract: smart_contract_v2, - }, + PrecompileCall::withdraw_unbonded {}, ) .expect_no_logs() .execute_returns(true); @@ -382,43 +622,40 @@ fn claim_bonus_reward_is_ok() { assert_eq!(events.len(), 1); assert_matches!( events[0].clone(), - pallet_dapp_staking_v3::Event::BonusReward { smart_contract, .. } if smart_contract == smart_contract + pallet_dapp_staking_v3::Event::ClaimedUnlocked { + amount, + .. + } if amount == unlock_amount ); }); } #[test] -fn claim_dapp_reward_is_ok() { +fn claim_dapp_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Register a dApp and stake on it + // Register a dApp for staking let staker_h160 = ALICE; - let smart_contract_address = [0xAF; 32]; - let smart_contract = ::SmartContract::wasm( - smart_contract_address.into(), - ); - let amount = 1234; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; register_and_stake(staker_h160, smart_contract.clone(), amount); - // Advance to 3rd era so we claim rewards for the 2nd era + // Advance enough eras so we can claim dApp reward advance_to_era(3); + let claim_era = 2; - let smart_contract_v2 = SmartContractV2 { - contract_type: SmartContractTypes::Wasm, - address: smart_contract_address.into(), - }; - - // Claim dApp reward and verify event - let claim_era: EraNumber = 2; + // Execute legacy call, expect dApp rewards to be claimed System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::claim_dapp_reward { - smart_contract: smart_contract_v2, - era: claim_era.into(), + PrecompileCall::claim_dapp { + contract_h160: smart_contract_address.into(), + era: claim_era, }, ) .expect_no_logs() @@ -428,23 +665,69 @@ fn claim_dapp_reward_is_ok() { assert_eq!(events.len(), 1); assert_matches!( events[0].clone(), - pallet_dapp_staking_v3::Event::DAppReward { era, smart_contract, .. } if era == claim_era && smart_contract == smart_contract + pallet_dapp_staking_v3::Event::DAppReward { + era, + smart_contract, + .. + } if era as u128 == claim_era && smart_contract == smart_contract ); }); } #[test] -fn unstake_from_unregistered_is_ok() { +fn claim_staker_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Register a dApp and stake on it + // Register a dApp for staking let staker_h160 = ALICE; - let smart_contract_address = [0xAF; 32]; - let smart_contract = ::SmartContract::wasm( - smart_contract_address.into(), - ); - let amount = 1234; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance enough eras so we can claim dApp reward + let target_era = 5; + advance_to_era(target_era); + let number_of_claims = (2..target_era).count(); + + // Execute legacy call, expect dApp rewards to be claimed + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_staker { + _contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect multiple reward to be claimed + let events = dapp_staking_events(); + assert_eq!(events.len(), number_of_claims as usize); + for era in 2..target_era { + assert_matches!( + events[era as usize - 2].clone(), + pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era + ); + } + }); +} + +#[test] +fn withdraw_from_unregistered_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; register_and_stake(staker_h160, smart_contract.clone(), amount); // Unregister the dApp @@ -453,19 +736,14 @@ fn unstake_from_unregistered_is_ok() { smart_contract.clone() )); - let smart_contract_v2 = SmartContractV2 { - contract_type: SmartContractTypes::Wasm, - address: smart_contract_address.into(), - }; - - // Unstake from the unregistered dApp and verify event + // Execute legacy call, expect funds to be unstaked & withdrawn System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::unstake_from_unregistered { - smart_contract: smart_contract_v2, + PrecompileCall::withdraw_from_unregistered { + contract_h160: smart_contract_address.into(), }, ) .expect_no_logs() @@ -475,52 +753,115 @@ fn unstake_from_unregistered_is_ok() { assert_eq!(events.len(), 1); assert_matches!( events[0].clone(), - pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { smart_contract, amount, .. } if smart_contract == smart_contract && amount == amount + pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount ); }); } #[test] -fn cleanup_expired_entries_is_ok() { +fn nomination_transfer_is_ok() { ExternalityBuilder::build().execute_with(|| { initialize(); - // Advance over to the Build&Earn subperiod - advance_to_next_subperiod(); - assert_eq!( - ActiveProtocolState::::get().subperiod(), - Subperiod::BuildAndEarn, - "Sanity check." - ); - - // Register a dApp and stake on it + // Register the first dApp, and stke on it. let staker_h160 = ALICE; - let smart_contract_address = [0xAF; 32]; - let smart_contract = ::SmartContract::wasm( - smart_contract_address.into(), + let staker_native = AddressMapper::into_account_id(staker_h160); + let smart_contract_address_1 = H160::repeat_byte(0xFA); + let smart_contract_1 = + ::SmartContract::evm(smart_contract_address_1); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract_1.clone(), amount); + + // Register the second dApp. + let smart_contract_address_2 = H160::repeat_byte(0xBF); + let smart_contract_2 = + ::SmartContract::evm(smart_contract_address_2); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + staker_native.clone(), + smart_contract_2.clone() + )); + + // 1st scenario - transfer enough amount from the first to second dApp to cover the stake, + // but not enough for full unstake. + let minimum_stake_amount: Balance = + ::MinimumStakeAmount::get(); + + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::nomination_transfer { + origin_contract_h160: smart_contract_address_1.into(), + amount: minimum_stake_amount, + target_contract_h160: smart_contract_address_2.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect the same amount to be staked on the second contract + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_1 && amount == minimum_stake_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_2 && amount == minimum_stake_amount ); - let amount = 1234; - register_and_stake(staker_h160, smart_contract.clone(), amount); - // Advance over to the next period so the entry for dApp becomes expired - advance_to_next_period(); + // 2nd scenario - transfer almost the entire amount from the first to second dApp. + // The amount is large enough to trigger full unstake of the first contract. + let unstake_amount = amount - minimum_stake_amount - 1; + let expected_stake_unstake_amount = amount - minimum_stake_amount; - // Cleanup single expired entry and verify event System::reset_events(); precompiles() .prepare_test( staker_h160, precompile_address(), - PrecompileCall::cleanup_expired_entries {}, + PrecompileCall::nomination_transfer { + origin_contract_h160: smart_contract_address_1.into(), + amount: unstake_amount, + target_contract_h160: smart_contract_address_2.into(), + }, ) .expect_no_logs() .execute_returns(true); + // We expect the same amount to be staked on the second contract let events = dapp_staking_events(); - assert_eq!(events.len(), 1); + assert_eq!(events.len(), 2); assert_matches!( events[0].clone(), - pallet_dapp_staking_v3::Event::ExpiredEntriesRemoved { count, .. } if count == 1 + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_1 && amount == expected_stake_unstake_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_2 && amount == expected_stake_unstake_amount ); }); } diff --git a/precompiles/dapp-staking-v3/src/test/tests_v3.rs b/precompiles/dapp-staking-v3/src/test/tests_v3.rs new file mode 100644 index 0000000000..5977417b5e --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/tests_v3.rs @@ -0,0 +1,526 @@ +// 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 . + +extern crate alloc; +use crate::{test::mock::*, *}; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use precompile_utils::testing::*; +use sp_core::H160; + +use assert_matches::assert_matches; + +use astar_primitives::{dapp_staking::CycleConfiguration, BlockNumber}; +use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber}; + +#[test] +fn protocol_state_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Prepare some mixed state in the future so not all entries are 'zero' + advance_to_next_period(); + advance_to_next_era(); + + let state = ActiveProtocolState::::get(); + + let expected_outcome = PrecompileProtocolState { + era: state.era.into(), + period: state.period_number().into(), + subperiod: subperiod_id(&state.subperiod()), + }; + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::protocol_state {}, + ) + .expect_no_logs() + .execute_returns(expected_outcome); + }); +} + +#[test] +fn unlocking_period_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let unlocking_period_in_eras: EraNumber = + ::UnlockingPeriod::get(); + let era_length: BlockNumber = + ::CycleConfiguration::blocks_per_era(); + + let expected_outcome = era_length * unlocking_period_in_eras; + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::unlocking_period {}, + ) + .expect_no_logs() + .execute_returns(expected_outcome); + }); +} + +#[test] +fn lock_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Lock some amount and verify event + let amount = 1234; + System::reset_events(); + precompiles() + .prepare_test(ALICE, precompile_address(), PrecompileCall::lock { amount }) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Locked { + amount, + .. + } if amount == amount + ); + }); +} + +#[test] +fn unlock_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let lock_amount = 1234; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + lock_amount, + )); + + // Unlock some amount and verify event + System::reset_events(); + let unlock_amount = 1234 / 7; + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::unlock { + amount: unlock_amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unlocking { + amount, + .. + } if amount == unlock_amount + ); + }); +} + +#[test] +fn claim_unlocked_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Lock/unlock some amount to create unlocking chunk + let amount = 1234; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + amount, + )); + assert_ok!(DappStaking::unlock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + amount, + )); + + // Advance enough into time so unlocking chunk can be claimed + let unlock_block = + Ledger::::get(&AddressMapper::into_account_id(ALICE)).unlocking[0].unlock_block; + run_to_block(unlock_block); + + // Claim unlocked chunk and verify event + System::reset_events(); + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::claim_unlocked {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ClaimedUnlocked { + amount, + .. + } if amount == amount + ); + }); +} + +#[test] +fn stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_h160 = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_h160); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock some amount which will be used for staking + let amount = 2000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: smart_contract_h160.as_bytes().try_into().unwrap(), + }; + + // Stake some amount and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::stake { + smart_contract: smart_contract_v2, + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn unstake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock & stake some amount + let amount = 2000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + assert_ok!(DappStaking::stake( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + smart_contract.clone(), + amount, + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Unstake some amount and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unstake { + smart_contract: smart_contract_v2, + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn claim_staker_rewards_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance a few eras so we can claim a few rewards + let target_era = 7; + advance_to_era(target_era); + let number_of_claims = (2..target_era).count(); + + // Claim staker rewards and verify events + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_staker_rewards {}, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect multiple reward to be claimed + let events = dapp_staking_events(); + assert_eq!(events.len(), number_of_claims as usize); + for era in 2..target_era { + assert_matches!( + events[era as usize - 2].clone(), + pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era + ); + } + }); +} + +#[test] +fn claim_bonus_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it, loyally + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance to the next period + advance_to_next_period(); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Claim bonus reward and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_bonus_reward { + smart_contract: smart_contract_v2, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::BonusReward { smart_contract, .. } if smart_contract == smart_contract + ); + }); +} + +#[test] +fn claim_dapp_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance to 3rd era so we claim rewards for the 2nd era + advance_to_era(3); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Claim dApp reward and verify event + let claim_era: EraNumber = 2; + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_dapp_reward { + smart_contract: smart_contract_v2, + era: claim_era.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::DAppReward { era, smart_contract, .. } if era == claim_era && smart_contract == smart_contract + ); + }); +} + +#[test] +fn unstake_from_unregistered_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unregister the dApp + assert_ok!(DappStaking::unregister( + RawOrigin::Root.into(), + smart_contract.clone() + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Unstake from the unregistered dApp and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unstake_from_unregistered { + smart_contract: smart_contract_v2, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { smart_contract, amount, .. } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn cleanup_expired_entries_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Advance over to the Build&Earn subperiod + advance_to_next_subperiod(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check." + ); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance over to the next period so the entry for dApp becomes expired + advance_to_next_period(); + + // Cleanup single expired entry and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::cleanup_expired_entries {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ExpiredEntriesRemoved { count, .. } if count == 1 + ); + }); +}