From cfb3dfaae215d7f279aa49a34298fcac6fea3bf4 Mon Sep 17 00:00:00 2001
From: Liam Aharon <liam.aharon@hotmail.com>
Date: Wed, 3 Apr 2024 15:59:19 +0400
Subject: [PATCH] update docs

---
 substrate/frame/asset-rewards/src/lib.rs   |  54 +++++++--
 substrate/frame/asset-rewards/src/tests.rs | 121 +++++++++++++++++++++
 2 files changed, 165 insertions(+), 10 deletions(-)

diff --git a/substrate/frame/asset-rewards/src/lib.rs b/substrate/frame/asset-rewards/src/lib.rs
index 35e1eaa58f3..50ce231bc41 100644
--- a/substrate/frame/asset-rewards/src/lib.rs
+++ b/substrate/frame/asset-rewards/src/lib.rs
@@ -23,18 +23,23 @@
 //!
 //! Governance can create a new incentive program for a fungible asset by creating a new pool.
 //!
-//! When creating the pool, governance specifies a 'staking asset', 'reward asset', and 'reward rate
-//! per block'.
+//! When creating the pool, governance specifies a 'staking asset', 'reward asset', 'reward rate
+//! per block', and an 'expiry block'.
 //!
-//! Once the pool is created, holders of the 'staking asset' can stake them in this pallet (creating
-//! a new Freeze). Once staked, the staker begins accumulating the right to claim the 'reward asset'
-//! each block, proportional to their share of the total staked tokens in the pool.
+//! Once the pool is created, holders of the 'staking asset' can stake them in this pallet, which
+//! puts a Freeze on the asset.
+//!
+//! Once staked, the staker begins accumulating the right to claim the 'reward asset' each block,
+//! proportional to their share of the total staked tokens in the pool.
 //!
 //! Reward assets pending distribution are held in an account derived from the pallet ID and a
 //! unique pool ID.
 //!
 //! Care should be taken to keep pool accounts adequately funded with the reward asset.
 //!
+//! The pool administator can adjust the reward rate per block, the expiry block, and the admin
+//! after the pool is created.
+//!
 //! ## Permissioning
 //!
 //! Currently, pool creation and management is permissioned and restricted to a configured Origin.
@@ -50,14 +55,23 @@
 //! pallet Call method, which while slightly more verbose, makes it much easier to understand the
 //! code and reason about where side-effects occur in the pallet.
 //!
-//! ## Implementation Notes
+//! ## Rewards Algorithm
 //!
-//! The implementation is based on the [AccumulatedRewardsPerShare](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f) algorithm.
+//! The rewards algorithm is based on the Synthetix [StakingRewards.sol](https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol)
+//! smart contract.
 //!
-//! Rewards are calculated JIT (just-in-time), when a staker claims their rewards.
+//! Rewards are calculated JIT (just-in-time), and all operations are O(1) making the approach
+//! scalable to many pools and stakers.
 //!
-//! All operations are O(1), allowing the approach to scale to an arbitrary amount of pools and
-//! stakers.
+//! The approach is widly used across the Ethereum ecosystem, there is also quite battle tested.
+//!
+//! ### Resources
+//!
+//! - [This YouTube video series](https://www.youtube.com/watch?v=6ZO5aYg1GI8), which walks through
+//!   the math of the algorithm.
+//! - [This dev.to article](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f),
+//!   which explains the algorithm of the SushiSwap MasterChef staking. While not identical to the
+//!   Synthetix approach, they are very similar.
 #![deny(missing_docs)]
 #![cfg_attr(not(feature = "std"), no_std)]
 
@@ -572,6 +586,26 @@ pub mod pallet {
 			)?;
 			Ok(())
 		}
+
+		/// Permissioned method to withdraw reward tokens from a pool.
+		pub fn withdraw_reward_tokens(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			amount: T::Balance,
+		) -> DispatchResult {
+			let caller = ensure_signed(origin)?;
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			ensure!(pool_info.admin == caller, BadOrigin);
+			T::Assets::transfer(
+				pool_info.reward_asset_id,
+				&caller,
+				&Self::pool_account_id(&pool_id)?,
+				amount,
+				Preservation::Preserve,
+			)?;
+
+			Ok(())
+		}
 	}
 
 	impl<T: Config> Pallet<T> {
diff --git a/substrate/frame/asset-rewards/src/tests.rs b/substrate/frame/asset-rewards/src/tests.rs
index 8de62643b59..79884096202 100644
--- a/substrate/frame/asset-rewards/src/tests.rs
+++ b/substrate/frame/asset-rewards/src/tests.rs
@@ -921,6 +921,127 @@ mod deposit_reward_tokens {
 	}
 }
 
+mod withdraw_reward_tokens {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let pool_id = 0;
+			let reward_asset_id = NativeOrWithId::<u32>::Native;
+			let initial_deposit = 10;
+			let withdraw_amount = 5;
+			create_default_pool();
+			let pool_account_id = StakingRewards::pool_account_id(&pool_id).unwrap();
+
+			let admin_balance_before =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &admin);
+			let pool_balance_before = <<MockRuntime as Config>::Assets>::balance(
+				reward_asset_id.clone(),
+				&pool_account_id,
+			);
+
+			// Deposit initial reward tokens
+			assert_ok!(StakingRewards::deposit_reward_tokens(
+				RuntimeOrigin::signed(admin),
+				pool_id,
+				initial_deposit
+			));
+
+			// Withdraw some tokens
+			assert_ok!(StakingRewards::withdraw_reward_tokens(
+				RuntimeOrigin::signed(admin),
+				pool_id,
+				withdraw_amount
+			));
+
+			let admin_balance_after =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &admin);
+			let pool_balance_after =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id, &pool_account_id);
+
+			assert_eq!(
+				<<MockRuntime as Config>::Assets>::balance(
+					reward_asset_id.clone(),
+					&pool_account_id
+				),
+				initial_deposit - withdraw_amount
+			);
+			assert_eq!(
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id, &admin),
+				withdraw_amount
+			);
+		});
+	}
+
+	// #[test]
+	// fn fails_for_non_existent_pool() {
+	// 	new_test_ext().execute_with(|| {
+	// 		let admin = 1;
+	// 		let non_existent_pool_id = 999;
+	// 		let withdraw_amount = 5000;
+	//
+	// 		assert_err!(
+	// 			StakingRewards::withdraw_reward_tokens(
+	// 				RuntimeOrigin::signed(admin),
+	// 				non_existent_pool_id,
+	// 				withdraw_amount
+	// 			),
+	// 			Error::<MockRuntime>::NonExistentPool
+	// 		);
+	// 	});
+	// }
+	//
+	// #[test]
+	// fn fails_for_non_admin() {
+	// 	new_test_ext().execute_with(|| {
+	// 		let non_admin = 2;
+	// 		let pool_id = 0;
+	// 		let withdraw_amount = 5000;
+	// 		create_default_pool();
+	//
+	// 		assert_err!(
+	// 			StakingRewards::withdraw_reward_tokens(
+	// 				RuntimeOrigin::signed(non_admin),
+	// 				pool_id,
+	// 				withdraw_amount
+	// 			),
+	// 			BadOrigin
+	// 		);
+	// 	});
+	// }
+	//
+	// #[test]
+	// fn fails_for_insufficient_pool_balance() {
+	// 	new_test_ext().execute_with(|| {
+	// 		let admin = 1;
+	// 		let pool_id = 0;
+	// 		let reward_asset_id = NativeOrWithId::<u32>::Native;
+	// 		let initial_deposit = 10000;
+	// 		let withdraw_amount = 15000;
+	// 		create_default_pool();
+	//
+	// 		// Deposit initial reward tokens
+	// 		let pool_account = StakingRewards::pool_account_id(&pool_id).unwrap();
+	// 		<<MockRuntime as Config>::Assets>::set_balance(
+	// 			reward_asset_id,
+	// 			&pool_account,
+	// 			initial_deposit,
+	// 		);
+	//
+	// 		assert_err!(
+	// 			StakingRewards::withdraw_reward_tokens(
+	// 				RuntimeOrigin::signed(admin),
+	// 				pool_id,
+	// 				withdraw_amount
+	// 			),
+	// 			assets::Error::<MockRuntime>::BalanceLow
+	// 		);
+	// 	});
+	// }
+}
+
 /// This integration test
 /// 1. Considers 2 stakers each staking and unstaking at different intervals, asserts their
 ///    claimable rewards are adjusted as expected, and that harvesting works.
-- 
GitLab