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