From 13cccba9605a60a16f44a821199445b5329566b5 Mon Sep 17 00:00:00 2001 From: Liam Aharon <liam.aharon@hotmail.com> Date: Tue, 2 Apr 2024 18:51:56 +0400 Subject: [PATCH] test reward and expiry adjustment --- substrate/frame/asset-rewards/src/lib.rs | 58 ++-- substrate/frame/asset-rewards/src/tests.rs | 305 ++++++++++++--------- 2 files changed, 212 insertions(+), 151 deletions(-) diff --git a/substrate/frame/asset-rewards/src/lib.rs b/substrate/frame/asset-rewards/src/lib.rs index afb91f42170..b031d9ea424 100644 --- a/substrate/frame/asset-rewards/src/lib.rs +++ b/substrate/frame/asset-rewards/src/lib.rs @@ -80,7 +80,7 @@ pub type PoolId = u32; pub(crate) const PRECISION_SCALING_FACTOR: u32 = u32::MAX; /// A pool staker. -#[derive(Debug, Default, Decode, Encode, MaxEncodedLen, TypeInfo)] +#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)] pub struct PoolStakerInfo<Balance> { /// Amount of tokens staked. amount: Balance, @@ -91,7 +91,7 @@ pub struct PoolStakerInfo<Balance> { } /// A staking pool. -#[derive(Debug, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> { /// The asset that is staked in this pool. staking_asset_id: AssetId, @@ -120,7 +120,10 @@ pub mod pallet { traits::tokens::{AssetId, Preservation}, }; use frame_system::pallet_prelude::*; - use sp_runtime::traits::{AccountIdConversion, BadOrigin, EnsureDiv, Saturating}; + use sp_runtime::{ + traits::{AccountIdConversion, BadOrigin, EnsureDiv, Saturating}, + DispatchResult, + }; #[pallet::pallet] pub struct Pallet<T>(_); @@ -463,11 +466,12 @@ pub mod pallet { new_reward_rate_per_block: T::Balance, ) -> DispatchResult { let caller = ensure_signed(origin)?; - let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?; + let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?; ensure!(pool_info.admin == caller, BadOrigin); Self::update_pool_rewards(&pool_id, None)?; + let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?; pool_info.reward_rate_per_block = new_reward_rate_per_block; Pools::<T>::insert(pool_id, pool_info); @@ -497,7 +501,7 @@ pub mod pallet { Ok(()) } - /// Modify a pool admin. + /// Modify a expiry block. /// /// TODO: Actually handle this in code pub fn set_pool_expiry_block( @@ -512,6 +516,8 @@ pub mod pallet { Error::<T>::ExpiryBlockMustBeInTheFuture ); + Self::update_pool_rewards(&pool_id, None)?; + let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?; ensure!(pool_info.admin == caller, BadOrigin); pool_info.expiry_block = new_expiry_block; @@ -557,25 +563,34 @@ pub mod pallet { } /// Update pool reward state, and optionally also a staker's rewards. + /// + /// Returns the updated pool info and optional staker info. pub fn update_pool_rewards( pool_id: &PoolId, staker: Option<&T::AccountId>, - ) -> DispatchResult { + ) -> Result< + ( + PoolInfo<T::AccountId, T::AssetId, T::Balance, BlockNumberFor<T>>, + Option<PoolStakerInfo<T::Balance>>, + ), + DispatchError, + > { let reward_per_token = Self::reward_per_token(pool_id)?; let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?; pool_info.last_update_block = frame_system::Pallet::<T>::block_number(); pool_info.reward_per_token_stored = reward_per_token; - Pools::<T>::insert(pool_id, pool_info); + Pools::<T>::insert(pool_id, pool_info.clone()); if let Some(staker) = staker { let mut staker_info = PoolStakers::<T>::get(pool_id, staker).unwrap_or_default(); staker_info.rewards = Self::derive_rewards(pool_id, staker)?; staker_info.reward_per_token_paid = reward_per_token; - PoolStakers::<T>::insert(pool_id, staker, staker_info); + PoolStakers::<T>::insert(pool_id, staker, staker_info.clone()); + return Ok((pool_info, Some(staker_info))); } - Ok(()) + Ok((pool_info, None)) } /// Derives the current reward per token for this pool. @@ -588,18 +603,19 @@ pub mod pallet { return Ok(pool_info.reward_per_token_stored) } - let blocks_elapsed: u32 = match frame_system::Pallet::<T>::block_number() - .saturating_sub(pool_info.last_update_block) - .try_into() - { - Ok(b) => b, - Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()), - }; + let rewardable_blocks_elapsed: u32 = + match Self::last_block_reward_applicable(pool_info.expiry_block) + .saturating_sub(pool_info.last_update_block) + .try_into() + { + Ok(b) => b, + Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()), + }; Ok(pool_info.reward_per_token_stored.saturating_add( pool_info .reward_rate_per_block - .saturating_mul(blocks_elapsed.into()) + .saturating_mul(rewardable_blocks_elapsed.into()) .saturating_mul(PRECISION_SCALING_FACTOR.into()) .ensure_div(pool_info.total_tokens_staked)?, )) @@ -621,5 +637,13 @@ pub mod pallet { .ensure_div(PRECISION_SCALING_FACTOR.into())? .saturating_add(staker_info.rewards)) } + + fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> { + if frame_system::Pallet::<T>::block_number() < pool_expiry_block { + frame_system::Pallet::<T>::block_number() + } else { + pool_expiry_block + } + } } } diff --git a/substrate/frame/asset-rewards/src/tests.rs b/substrate/frame/asset-rewards/src/tests.rs index 0e1f9e921c6..8258a036ad9 100644 --- a/substrate/frame/asset-rewards/src/tests.rs +++ b/substrate/frame/asset-rewards/src/tests.rs @@ -573,139 +573,176 @@ mod set_pool_expiry_block { } } -mod integration { - use super::*; - - /// Assert that an amount has been hypothetically earned by a staker. - fn assert_hypothetically_earned( - staker: u128, - expected_earned: u128, - pool_id: u32, - reward_asset_id: NativeOrWithId<u32>, - ) { - hypothetically!({ - // Get the pre-harvest balance. - let balance_before: <MockRuntime as Config>::Balance = - <<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker); - - // Harvest the rewards. - assert_ok!(StakingRewards::harvest_rewards( - RuntimeOrigin::signed(staker), - pool_id, - None - )); - - // Sanity check: staker rewards are reset to 0. - assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, staker).unwrap().rewards, 0); - - // Check that the staker has earned the expected amount. - let balance_after = - <<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker); - assert_eq!( - balance_after - balance_before, - <u128 as Into<<MockRuntime as Config>::Balance>>::into(expected_earned) - ); - }); - } - - #[test] - /// In this integration test scenario, we will consider 2 stakers each staking and unstaking at - /// different intervals, and assert their claimable rewards are as expected. - /// - /// Note: There are occasionally off by 1 errors due to rounding. In practice, this is - /// insignificant. - fn two_stakers() { - new_test_ext().execute_with(|| { - // Setup - let admin = 1; - let staker1 = 10u128; - let staker2 = 20; - let staking_asset_id = NativeOrWithId::<u32>::WithId(1); - let reward_asset_id = NativeOrWithId::<u32>::Native; - let reward_rate_per_block = 100; - let expiry_block = 25u64.into(); - create_tokens(admin, vec![staking_asset_id.clone()]); - assert_ok!(StakingRewards::create_pool( - RuntimeOrigin::signed(admin), - Box::new(staking_asset_id.clone()), - Box::new(reward_asset_id.clone()), - reward_rate_per_block, - expiry_block, - None - )); - let pool_id = 0; - let pool_account_id = StakingRewards::pool_account_id(&pool_id).unwrap(); - <<MockRuntime as Config>::Assets>::set_balance( - reward_asset_id.clone(), - &pool_account_id, - 100_000, - ); - <<MockRuntime as Config>::Assets>::set_balance( - staking_asset_id.clone(), - &staker1, - 100_000, - ); - <<MockRuntime as Config>::Assets>::set_balance( - staking_asset_id.clone(), - &staker2, - 100_000, - ); +/// Assert that an amount has been hypothetically earned by a staker. +fn assert_hypothetically_earned( + staker: u128, + expected_earned: u128, + pool_id: u32, + reward_asset_id: NativeOrWithId<u32>, +) { + hypothetically!({ + // Get the pre-harvest balance. + let balance_before: <MockRuntime as Config>::Balance = + <<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker); + + // Harvest the rewards. + assert_ok!(StakingRewards::harvest_rewards(RuntimeOrigin::signed(staker), pool_id, None)); + + // Sanity check: staker rewards are reset to 0. + assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, staker).unwrap().rewards, 0); + + // Check that the staker has earned the expected amount. + let balance_after = + <<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker); + assert_eq!( + balance_after - balance_before, + <u128 as Into<<MockRuntime as Config>::Balance>>::into(expected_earned) + ); + }); +} - // Block 7: Staker 1 stakes 100 tokens. - System::set_block_number(7); - assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100)); - // At this point - // - Staker 1 has earned 0 tokens. - // - Staker 1 is earning 100 tokens per block. - - // Check that Staker 1 has earned 0 tokens. - assert_hypothetically_earned(staker1, 0, pool_id, reward_asset_id.clone()); - - // Block 9: Staker 2 stakes 100 tokens. - System::set_block_number(9); - assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker2), pool_id, 100)); - // At this point - // - Staker 1 has earned 200 (100*2) tokens. - // - Staker 2 has earned 0 tokens. - // - Staker 1 is earning 50 tokens per block. - // - Staker 2 is earning 50 tokens per block. - - // Check that Staker 1 has earned 200 tokens and Staker 2 has earned 0 tokens. - assert_hypothetically_earned(staker1, 200, pool_id, reward_asset_id.clone()); - assert_hypothetically_earned(staker2, 0, pool_id, reward_asset_id.clone()); - - // Block 12: Staker 1 stakes an additional 100 tokens. - System::set_block_number(12); - assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100)); - // At this point - // - Staker 1 has earned 350 (200 + (50 * 3)) tokens. - // - Staker 2 has earned 150 (50 * 3) tokens. - // - Staker 1 is earning 66.66 tokens per block. - // - Staker 2 is earning 33.33 tokens per block. - - // Check that Staker 1 has earned 350 tokens and Staker 2 has earned 150 tokens. - assert_hypothetically_earned(staker1, 349, pool_id, reward_asset_id.clone()); - assert_hypothetically_earned(staker2, 149, pool_id, reward_asset_id.clone()); - - // Block 22: Staker 1 unstakes 100 tokens. - System::set_block_number(22); - assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100)); - // - Staker 1 has earned 1016 (350 + 66.66 * 10) tokens. - // - Staker 2 has earned 483 (150 + 33.33 * 10) tokens. - // - Staker 1 is earning 50 tokens per block. - // - Staker 2 is earning 50 tokens per block. - assert_hypothetically_earned(staker1, 1015, pool_id, reward_asset_id.clone()); - assert_hypothetically_earned(staker2, 483, pool_id, reward_asset_id.clone()); - - // Block 23: Staker 1 unstakes 100 tokens. - System::set_block_number(23); - assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100)); - // - Staker 1 has earned 1065 (1015 + 50) tokens. - // - Staker 2 has earned 533 (483 + 50) tokens. - // - Staker 1 is earning 0 tokens per block. - // - Staker 2 is earning 100 tokens per block. - assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); - assert_hypothetically_earned(staker2, 533, pool_id, reward_asset_id.clone()); - }); - } +/// In this integration test scenario, we +/// 1. Consider 2 stakers each staking and unstaking at different intervals, and assert their +/// claimable rewards are as expected. +/// 2. Check that rewards are correctly halted after the pool's expiry block, and resume when the +/// pool is extended. +/// 3. Check that reward rates adjustment works correctly. +/// +/// Note: There are occasionally off by 1 errors due to rounding. In practice this is +/// insignificant. +#[test] +fn two_stakers_integration_test() { + new_test_ext().execute_with(|| { + // Setup + let admin = 1; + let staker1 = 10u128; + let staker2 = 20; + let staking_asset_id = NativeOrWithId::<u32>::WithId(1); + let reward_asset_id = NativeOrWithId::<u32>::Native; + let reward_rate_per_block = 100; + let expiry_block = 25u64.into(); + create_tokens(admin, vec![staking_asset_id.clone()]); + assert_ok!(StakingRewards::create_pool( + RuntimeOrigin::signed(admin), + Box::new(staking_asset_id.clone()), + Box::new(reward_asset_id.clone()), + reward_rate_per_block, + expiry_block, + None + )); + let pool_id = 0; + let pool_account_id = StakingRewards::pool_account_id(&pool_id).unwrap(); + <<MockRuntime as Config>::Assets>::set_balance( + reward_asset_id.clone(), + &pool_account_id, + 100_000, + ); + <<MockRuntime as Config>::Assets>::set_balance(staking_asset_id.clone(), &staker1, 100_000); + <<MockRuntime as Config>::Assets>::set_balance(staking_asset_id.clone(), &staker2, 100_000); + + // Block 7: Staker 1 stakes 100 tokens. + System::set_block_number(7); + assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100)); + // At this point + // - Staker 1 has earned 0 tokens. + // - Staker 1 is earning 100 tokens per block. + + // Check that Staker 1 has earned 0 tokens. + assert_hypothetically_earned(staker1, 0, pool_id, reward_asset_id.clone()); + + // Block 9: Staker 2 stakes 100 tokens. + System::set_block_number(9); + assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker2), pool_id, 100)); + // At this point + // - Staker 1 has earned 200 (100*2) tokens. + // - Staker 2 has earned 0 tokens. + // - Staker 1 is earning 50 tokens per block. + // - Staker 2 is earning 50 tokens per block. + + // Check that Staker 1 has earned 200 tokens and Staker 2 has earned 0 tokens. + assert_hypothetically_earned(staker1, 200, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 0, pool_id, reward_asset_id.clone()); + + // Block 12: Staker 1 stakes an additional 100 tokens. + System::set_block_number(12); + assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100)); + // At this point + // - Staker 1 has earned 350 (200 + (50 * 3)) tokens. + // - Staker 2 has earned 150 (50 * 3) tokens. + // - Staker 1 is earning 66.66 tokens per block. + // - Staker 2 is earning 33.33 tokens per block. + + // Check that Staker 1 has earned 350 tokens and Staker 2 has earned 150 tokens. + assert_hypothetically_earned(staker1, 349, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 149, pool_id, reward_asset_id.clone()); + + // Block 22: Staker 1 unstakes 100 tokens. + System::set_block_number(22); + assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100)); + // - Staker 1 has earned 1016 (350 + 66.66 * 10) tokens. + // - Staker 2 has earned 483 (150 + 33.33 * 10) tokens. + // - Staker 1 is earning 50 tokens per block. + // - Staker 2 is earning 50 tokens per block. + assert_hypothetically_earned(staker1, 1015, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 483, pool_id, reward_asset_id.clone()); + + // Block 23: Staker 1 unstakes 100 tokens. + System::set_block_number(23); + assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100)); + // - Staker 1 has earned 1065 (1015 + 50) tokens. + // - Staker 2 has earned 533 (483 + 50) tokens. + // - Staker 1 is earning 0 tokens per block. + // - Staker 2 is earning 100 tokens per block. + assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 533, pool_id, reward_asset_id.clone()); + + // Block 50: Stakers should only have earned 2 blocks worth of tokens (expiry is 25). + System::set_block_number(50); + // - Staker 1 has earned 1065 tokens. + // - Staker 2 has earned 733 (533 + 2 * 100) tokens. + // - Staker 1 is earning 0 tokens per block. + // - Staker 2 is earning 0 tokens per block. + assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 733, pool_id, reward_asset_id.clone()); + + // Block 51: Extend the pool expiry block to 60. + System::set_block_number(51); + // - Staker 1 is earning 0 tokens per block. + // - Staker 2 is earning 100 tokens per block. + assert_ok!(StakingRewards::set_pool_expiry_block( + RuntimeOrigin::signed(admin), + pool_id, + 60u64 + )); + assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 733, pool_id, reward_asset_id.clone()); + + // Block 53: Check rewards are resumed. + // - Staker 1 has earned 1065 tokens. + // - Staker 2 has earned 933 (733 + 2 * 100) tokens. + // - Staker 2 is earning 100 tokens per block. + System::set_block_number(53); + assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 933, pool_id, reward_asset_id.clone()); + + // Block 55: Halve the block reward. + // - Staker 1 has earned 1065 tokens. + // - Staker 2 has earned 1133 (933 + 2 * 100) tokens. + // - Staker 2 is earning 50 tokens per block. + System::set_block_number(55); + assert_ok!(StakingRewards::set_pool_reward_rate_per_block( + RuntimeOrigin::signed(admin), + pool_id, + 50 + )); + assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 1133, pool_id, reward_asset_id.clone()); + + // Block 60: Check rewards were adjusted correctly. + // - Staker 1 has earned 1065 tokens. + // - Staker 2 has earned 1383 (1133 + 5 * 50) tokens. + System::set_block_number(60); + assert_hypothetically_earned(staker1, 1064, pool_id, reward_asset_id.clone()); + assert_hypothetically_earned(staker2, 1383, pool_id, reward_asset_id.clone()); + }); } -- GitLab