From 0daae3a68e65609fe2c443d731576766cfc87487 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 13 Oct 2023 11:42:04 +0200 Subject: [PATCH] Test utils for claim-staker --- pallets/dapp-staking-v3/src/lib.rs | 22 +-- pallets/dapp-staking-v3/src/test/mock.rs | 15 ++ .../dapp-staking-v3/src/test/testing_utils.rs | 136 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 1 + 4 files changed, 164 insertions(+), 10 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8242cdf20c..2fd93007e1 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -352,7 +352,7 @@ pub mod pallet { } let mut era_info = CurrentEraInfo::::get(); - let staker_reward_pool = Balance::from(1000_u128); // TODO: calculate this properly + let staker_reward_pool = Balance::from(1000_u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) let era_reward = EraReward::new(staker_reward_pool, era_info.total_staked_amount()); let ending_era = protocol_state.era; let next_era = ending_era.saturating_add(1); @@ -985,13 +985,17 @@ pub mod pallet { /// TODO #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] - pub fn claim_staker_reward(origin: OriginFor) -> DispatchResult { + pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; let protocol_state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); + // TODO: how do we handle expired rewards? Add an additional call to clean them up? + // Putting this logic inside existing calls will add even more complexity. + + // Check if the rewards have expired let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; ensure!( staked_period @@ -1026,9 +1030,11 @@ pub mod pallet { .staked .left_split(last_claim_era) .map_err(|_| Error::::InternalClaimStakerError)?; + ensure!(chunks_for_claim.0.len() > 0, Error::::NoClaimableRewards); // Calculate rewards let mut rewards: Vec<_> = Vec::new(); + let mut reward_sum = Balance::zero(); for era in first_claim_era..=last_claim_era { let era_reward = era_rewards .get(era) @@ -1041,14 +1047,14 @@ pub mod pallet { let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) * era_reward.staker_reward_pool(); rewards.push((era, staker_reward)); + reward_sum.saturating_accrue(staker_reward); } - let reward_sum = rewards.iter().fold(Balance::zero(), |acc, (_, reward)| { - acc.saturating_add(*reward) - }); - - // TODO; update & write ledger - if is_full_period_claimed {} + // Write updated ledger back to storage + if is_full_period_claimed { + ledger.all_stake_rewards_claimed(); + } + Self::update_ledger(&account, ledger); T::Currency::deposit_into_existing(&account, reward_sum) .map_err(|_| Error::::InternalClaimStakerError)?; diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index bb287172a4..e2d6e1e185 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -235,3 +235,18 @@ pub(crate) fn _advance_to_next_period_type() { run_for_blocks(1); } } + +// Return all dApp staking events from the event buffer. +pub fn dapp_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let RuntimeEvent::DappStaking(inner) = e { + Some(inner) + } else { + None + } + }) + .collect() +} diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 532d856f05..0d6e1f0c8d 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -20,11 +20,12 @@ use crate::test::mock::*; use crate::types::*; use crate::{ pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, ContractStake, - CurrentEraInfo, DAppId, Event, IntegratedDApps, Ledger, NextDAppId, StakerInfo, + CurrentEraInfo, DAppId, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, + PeriodEndInfo, StakerInfo, }; use frame_support::{assert_ok, traits::Get}; -use sp_runtime::traits::Zero; +use sp_runtime::{traits::Zero, Perbill}; use std::collections::HashMap; /// Helper struct used to store the entire pallet state snapshot. @@ -48,6 +49,11 @@ pub(crate) struct MemorySnapshot { >, contract_stake: HashMap<::SmartContract, ContractStakingInfoSeries>, + era_rewards: HashMap< + EraNumber, + EraRewardSpan<::EraRewardSpanLength>, + >, + period_end: HashMap, } impl MemorySnapshot { @@ -63,6 +69,8 @@ impl MemorySnapshot { .map(|(k1, k2, v)| ((k1, k2), v)) .collect(), contract_stake: ContractStake::::iter().collect(), + era_rewards: EraRewards::::iter().collect(), + period_end: PeriodEnd::::iter().collect(), } } @@ -742,3 +750,127 @@ pub(crate) fn assert_unstake( ); } } + +/// Claim staker rewards. +pub(crate) fn assert_claim_staker_rewards(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(&account); + + // Get the first eligible era for claiming rewards + let first_claim_era = pre_ledger + .staked + .0 + .first() + .expect("Entry must exist, otherwise 'claim' is invalid.") + .get_era(); + + // Get the apprropriate era rewards span for the 'first era' + let era_span_length: EraNumber = + ::EraRewardSpanLength::get(); + let era_span_index = first_claim_era - (first_claim_era % era_span_length); + let era_rewards_span = pre_snapshot + .era_rewards + .get(&era_span_index) + .expect("Entry must exist, otherwise 'claim' is invalid."); + + // Calculate the final era for claiming rewards. Also determine if this will fully claim all staked period rewards. + let is_current_period_stake = match pre_ledger.staked_period { + Some(staked_period) + if staked_period == pre_snapshot.active_protocol_state.period_number() => + { + true + } + _ => false, + }; + + let (last_claim_era, is_full_claim) = if is_current_period_stake { + (pre_snapshot.active_protocol_state.era - 1, false) + } else { + let last_claim_era = era_rewards_span.last_era(); + + let claim_period = pre_ledger.staked_period.unwrap(); + let period_end = pre_snapshot + .period_end + .get(&claim_period) + .expect("Entry must exist, since it's a past period."); + + let last_claim_era = era_rewards_span.last_era().min(period_end.final_era); + let is_full_claim = last_claim_era == period_end.final_era; + (last_claim_era, is_full_claim) + }; + + assert!( + last_claim_era < pre_snapshot.active_protocol_state.era, + "Sanity check." + ); + + // Calculate the expected rewards + let mut rewards = Vec::new(); + for era in first_claim_era..=last_claim_era { + let era_reward_info = era_rewards_span + .get(era) + .expect("Entry must exist, otherwise 'claim' is invalid."); + let stake_chunk = pre_ledger + .staked + .get(era) + .expect("Entry must exist, otherwise 'claim' is invalid."); + + let reward = Perbill::from_rational(stake_chunk.amount, era_reward_info.staked()) + * era_reward_info.staker_reward_pool(); + rewards.push((era, reward)); + } + let total_reward = rewards + .iter() + .fold(Balance::zero(), |acc, (_, reward)| acc + reward); + + // Unstake from smart contract & verify event(s) + assert_ok!(DappStaking::claim_staker_rewards(RuntimeOrigin::signed( + account + ),)); + + let events = dapp_staking_events(); + assert_eq!(events.len(), rewards.len()); + for (event, (era, reward)) in events.iter().zip(rewards.iter()) { + assert_eq!( + event, + &Event::::Reward { + account, + era: *era, + amount: *reward, + } + ); + } + + // Verify post state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + total_reward, + "Total issuance must increase by the total reward amount." + ); + + let post_free_balance = ::Currency::free_balance(&account); + assert_eq!( + post_free_balance, + pre_free_balance + total_reward, + "Free balance must increase by the total reward amount." + ); + + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + + if is_full_claim { + assert!(post_ledger.staked.0.is_empty()); + assert!(post_ledger.staked_period.is_none()); + } else { + let stake_chunk = post_ledger.staked.0.first().expect("Entry must exist"); + assert_eq!(stake_chunk.era, last_claim_era + 1); + assert_eq!( + stake_chunk.amount, + pre_ledger.staked.get(last_claim_era).unwrap().amount + ); + } +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index d8c8b37999..875ece456c 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -789,6 +789,7 @@ where /// Notify ledger that all `stake` rewards have been claimed for the staked era. pub fn all_stake_rewards_claimed(&mut self) { + // TODO: improve handling once bonus reward tracking is added self.staked = SparseBoundedAmountEraVec(BoundedVec::::default()); self.staked_period = None; }