From 7a15551171fdd794afe5b38a503668c9c940029d Mon Sep 17 00:00:00 2001
From: Liam Aharon <liam.aharon@hotmail.com>
Date: Tue, 2 Apr 2024 11:01:01 +0400
Subject: [PATCH] add integration test

---
 substrate/frame/asset-rewards/Cargo.toml   |   2 +-
 substrate/frame/asset-rewards/src/lib.rs   |  36 +++---
 substrate/frame/asset-rewards/src/mock.rs  |   2 +-
 substrate/frame/asset-rewards/src/tests.rs | 142 ++++++++++++++++++++-
 4 files changed, 157 insertions(+), 25 deletions(-)

diff --git a/substrate/frame/asset-rewards/Cargo.toml b/substrate/frame/asset-rewards/Cargo.toml
index 346af2f6ffa..b0ef2c2199e 100644
--- a/substrate/frame/asset-rewards/Cargo.toml
+++ b/substrate/frame/asset-rewards/Cargo.toml
@@ -17,7 +17,7 @@ targets = ["x86_64-unknown-linux-gnu"]
 
 [dependencies]
 codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
-frame-support = { path = "../support", default-features = false }
+frame-support = { path = "../support", default-features = false, features = ["experimental"] }
 frame-system = { path = "../system", default-features = false }
 frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true }
 scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
diff --git a/substrate/frame/asset-rewards/src/lib.rs b/substrate/frame/asset-rewards/src/lib.rs
index c582ca3bf05..ef7187188ed 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(Default, Decode, Encode, MaxEncodedLen, TypeInfo)]
+#[derive(Debug, Default, Decode, Encode, MaxEncodedLen, TypeInfo)]
 pub struct PoolStakerInfo<Balance> {
 	/// Amount of tokens staked.
 	amount: Balance,
@@ -416,7 +416,7 @@ pub mod pallet {
 
 			// Emit event.
 			Self::deposit_event(Event::RewardsHarvested {
-				who: caller,
+				who: caller.clone(),
 				staker,
 				pool_id,
 				amount: staker_info.rewards,
@@ -424,6 +424,7 @@ pub mod pallet {
 
 			// Reset staker rewards.
 			staker_info.rewards = 0u32.into();
+			PoolStakers::<T>::insert(pool_id, &caller, staker_info);
 
 			Ok(())
 		}
@@ -479,7 +480,7 @@ pub mod pallet {
 
 	impl<T: Config> Pallet<T> {
 		/// Derive a pool account ID from the pallet's ID.
-		fn pool_account_id(id: &PoolId) -> Result<T::AccountId, DispatchError> {
+		pub fn pool_account_id(id: &PoolId) -> Result<T::AccountId, DispatchError> {
 			if Pools::<T>::contains_key(id) {
 				Ok(T::PalletId::get().into_sub_account_truncating(id))
 			} else {
@@ -488,18 +489,19 @@ pub mod pallet {
 		}
 
 		/// Update pool reward state.
-		fn update_pool_rewards(pool_id: &PoolId, staker: &T::AccountId) -> DispatchResult {
+		pub fn update_pool_rewards(pool_id: &PoolId, staker: &T::AccountId) -> DispatchResult {
 			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();
-			Pools::<T>::insert(pool_id, pool_info);
-
 			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);
 
+			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);
+
 			Ok(())
 		}
 
@@ -510,7 +512,7 @@ pub mod pallet {
 			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
 
 			if pool_info.total_tokens_staked.eq(&0u32.into()) {
-				return Ok(0u32.into());
+				return Ok(pool_info.reward_per_token_stored)
 			}
 
 			let blocks_elapsed: u32 = match frame_system::Pallet::<T>::block_number()
@@ -521,15 +523,13 @@ pub mod pallet {
 				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(PRECISION_SCALING_FACTOR.into()),
-				)
-				.ensure_div(pool_info.total_tokens_staked)?)
+			Ok(pool_info.reward_per_token_stored.saturating_add(
+				pool_info
+					.reward_rate_per_block
+					.saturating_mul(blocks_elapsed.into())
+					.saturating_mul(PRECISION_SCALING_FACTOR.into())
+					.ensure_div(pool_info.total_tokens_staked)?,
+			))
 		}
 
 		/// Derives the amount of rewards earned by a staker.
diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs
index 658d1bb6842..c179f0863da 100644
--- a/substrate/frame/asset-rewards/src/mock.rs
+++ b/substrate/frame/asset-rewards/src/mock.rs
@@ -157,7 +157,7 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
 	// .unwrap();
 
 	pallet_balances::GenesisConfig::<MockRuntime> {
-		balances: vec![(1, 10000), (2, 20000), (3, 30000), (4, 40000)],
+		balances: vec![(1, 10000), (2, 20000), (3, 30000), (4, 40000), (10, 40000), (20, 40000)],
 	}
 	.assimilate_storage(&mut t)
 	.unwrap();
diff --git a/substrate/frame/asset-rewards/src/tests.rs b/substrate/frame/asset-rewards/src/tests.rs
index 3e28f25f316..cf459e5af06 100644
--- a/substrate/frame/asset-rewards/src/tests.rs
+++ b/substrate/frame/asset-rewards/src/tests.rs
@@ -16,13 +16,10 @@
 // limitations under the License.
 
 use crate::{mock::*, *};
-use frame_support::{assert_err, assert_ok, traits::fungible::NativeOrWithId};
+use frame_support::{assert_err, assert_ok, hypothetically, traits::fungible::NativeOrWithId};
 
 fn create_tokens(owner: u128, tokens: Vec<NativeOrWithId<u32>>) {
-	create_tokens_with_ed(owner, tokens, 1)
-}
-
-fn create_tokens_with_ed(owner: u128, tokens: Vec<NativeOrWithId<u32>>, ed: u128) {
+	let ed = 1;
 	for token_id in tokens {
 		let asset_id = match token_id {
 			NativeOrWithId::WithId(id) => id,
@@ -412,3 +409,138 @@ mod unstake {
 		});
 	}
 }
+
+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;
+			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,
+				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());
+		});
+	}
+}
-- 
GitLab