pallet_asset_rewards/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # FRAME Staking Rewards Pallet
19//!
20//! Allows accounts to be rewarded for holding `fungible` asset/s, for example LP tokens.
21//!
22//! ## Overview
23//!
24//! Initiate an incentive program for a fungible asset by creating a new pool.
25//!
26//! During pool creation, a 'staking asset', 'reward asset', 'reward rate per block', 'expiry
27//! block', and 'admin' are specified.
28//!
29//! Once created, holders of the 'staking asset' can 'stake' them in a corresponding pool, which
30//! creates a Freeze on the asset.
31//!
32//! Once staked, rewards denominated in 'reward asset' begin accumulating to the staker,
33//! proportional to their share of the total staked tokens in the pool.
34//!
35//! Reward assets pending distribution are held in an account unique to each pool.
36//!
37//! Care should be taken by the pool operator to keep pool accounts adequately funded with the
38//! reward asset.
39//!
40//! The pool admin may increase reward rate per block, increase expiry block, and change admin.
41//!
42//! ## Disambiguation
43//!
44//! While this pallet shares some terminology with the `staking-pool` and similar native staking
45//! related pallets, it is distinct and is entirely unrelated to native staking.
46//!
47//! ## Permissioning
48//!
49//! Currently, pool creation and management restricted to a configured Origin.
50//!
51//! Future iterations of this pallet may allow permissionless creation and management of pools.
52//!
53//! Note: The permissioned origin must return an AccountId. This can be achieved for any Origin by
54//! wrapping it with `EnsureSuccess`.
55//!
56//! ## Implementation Notes
57//!
58//! Internal logic functions such as `update_pool_and_staker_rewards` were deliberately written
59//! without side-effects.
60//!
61//! Storage interaction such as reads and writes are instead all performed in the top level
62//! pallet Call method, which while slightly more verbose, makes it easier to understand the
63//! code and reason about how storage reads and writes occur in the pallet.
64//!
65//! ## Rewards Algorithm
66//!
67//! The rewards algorithm is based on the Synthetix [StakingRewards.sol](https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol)
68//! smart contract.
69//!
70//! Rewards are calculated JIT (just-in-time), and all operations are O(1) making the approach
71//! scalable to many pools and stakers.
72//!
73//! ### Resources
74//!
75//! - [This video series](https://www.youtube.com/watch?v=6ZO5aYg1GI8), which walks through the math
76//!   of the algorithm.
77//! - [This dev.to article](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f),
78//!   which explains the algorithm of the SushiSwap MasterChef staking. While not identical to the
79//!   Synthetix approach, they are quite similar.
80#![deny(missing_docs)]
81#![cfg_attr(not(feature = "std"), no_std)]
82
83pub use pallet::*;
84
85use codec::{Codec, Decode, Encode, MaxEncodedLen};
86use frame_support::{
87	traits::{
88		fungibles::{Inspect, Mutate},
89		schedule::DispatchTime,
90		tokens::Balance,
91	},
92	PalletId,
93};
94use frame_system::pallet_prelude::BlockNumberFor;
95use scale_info::TypeInfo;
96use sp_core::Get;
97use sp_runtime::{
98	traits::{MaybeDisplay, Zero},
99	DispatchError,
100};
101use sp_std::boxed::Box;
102
103#[cfg(feature = "runtime-benchmarks")]
104pub mod benchmarking;
105#[cfg(test)]
106mod mock;
107#[cfg(test)]
108mod tests;
109mod weights;
110
111pub use weights::WeightInfo;
112
113/// Unique id type for each pool.
114pub type PoolId = u32;
115
116/// Multiplier to maintain precision when calculating rewards.
117pub(crate) const PRECISION_SCALING_FACTOR: u16 = 4096;
118
119/// Convenience alias for `PoolInfo`.
120pub type PoolInfoFor<T> = PoolInfo<
121	<T as frame_system::Config>::AccountId,
122	<T as Config>::AssetId,
123	<T as Config>::Balance,
124	BlockNumberFor<T>,
125>;
126
127/// The state of a staker in a pool.
128#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
129pub struct PoolStakerInfo<Balance> {
130	/// Amount of tokens staked.
131	amount: Balance,
132	/// Accumulated, unpaid rewards.
133	rewards: Balance,
134	/// Reward per token value at the time of the staker's last interaction with the contract.
135	reward_per_token_paid: Balance,
136}
137
138/// The state and configuration of an incentive pool.
139#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
140pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
141	/// The asset staked in this pool.
142	staked_asset_id: AssetId,
143	/// The asset distributed as rewards by this pool.
144	reward_asset_id: AssetId,
145	/// The amount of tokens rewarded per block.
146	reward_rate_per_block: Balance,
147	/// The block the pool will cease distributing rewards.
148	expiry_block: BlockNumber,
149	/// The account authorized to manage this pool.
150	admin: AccountId,
151	/// The total amount of tokens staked in this pool.
152	total_tokens_staked: Balance,
153	/// Total rewards accumulated per token, up to the `last_update_block`.
154	reward_per_token_stored: Balance,
155	/// Last block number the pool was updated.
156	last_update_block: BlockNumber,
157	/// The account that holds the pool's rewards.
158	account: AccountId,
159}
160
161sp_api::decl_runtime_apis! {
162	/// The runtime API for the asset rewards pallet.
163	pub trait AssetRewards<Cost: MaybeDisplay + Codec> {
164		/// Get the cost of creating a pool.
165		///
166		/// This is especially useful when the cost is dynamic.
167		fn pool_creation_cost() -> Cost;
168	}
169}
170
171#[frame_support::pallet]
172pub mod pallet {
173	use super::*;
174	use frame_support::{
175		pallet_prelude::*,
176		traits::{
177			fungibles::MutateFreeze,
178			tokens::{AssetId, Fortitude, Preservation},
179			Consideration, Footprint,
180		},
181	};
182	use frame_system::pallet_prelude::*;
183	use sp_runtime::{
184		traits::{
185			AccountIdConversion, BadOrigin, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureMul,
186			EnsureSub, EnsureSubAssign,
187		},
188		DispatchResult,
189	};
190
191	#[pallet::pallet]
192	pub struct Pallet<T>(_);
193
194	/// A reason for the pallet placing a hold on funds.
195	#[pallet::composite_enum]
196	pub enum FreezeReason {
197		/// Funds are staked in the pallet.
198		#[codec(index = 0)]
199		Staked,
200	}
201
202	/// A reason for the pallet placing a hold on funds.
203	#[pallet::composite_enum]
204	pub enum HoldReason {
205		/// Cost associated with storing pool information on-chain.
206		#[codec(index = 0)]
207		PoolCreation,
208	}
209
210	#[pallet::config]
211	pub trait Config: frame_system::Config {
212		/// Overarching event type.
213		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
214
215		/// The pallet's unique identifier, used to derive the pool's account ID.
216		///
217		/// The account ID is derived once during pool creation and stored in the storage.
218		#[pallet::constant]
219		type PalletId: Get<PalletId>;
220
221		/// Identifier for each type of asset.
222		type AssetId: AssetId + Member + Parameter;
223
224		/// The type in which the assets are measured.
225		type Balance: Balance + TypeInfo;
226
227		/// The origin with permission to create pools.
228		///
229		/// The Origin must return an AccountId.
230		type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
231
232		/// Registry of assets that can be configured to either stake for rewards, or be offered as
233		/// rewards for staking.
234		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
235			+ Mutate<Self::AccountId>;
236
237		/// Freezer for the Assets.
238		type AssetsFreezer: MutateFreeze<
239			Self::AccountId,
240			Id = Self::RuntimeFreezeReason,
241			AssetId = Self::AssetId,
242			Balance = Self::Balance,
243		>;
244
245		/// The overarching freeze reason.
246		type RuntimeFreezeReason: From<FreezeReason>;
247
248		/// Means for associating a cost with the on-chain storage of pool information, which
249		/// is incurred by the pool creator.
250		///
251		/// The passed `Footprint` specifically accounts for the storage footprint of the pool's
252		/// information itself, excluding any potential storage footprint related to the stakers.
253		type Consideration: Consideration<Self::AccountId, Footprint>;
254
255		/// Weight information for extrinsics in this pallet.
256		type WeightInfo: WeightInfo;
257
258		/// Helper for benchmarking.
259		#[cfg(feature = "runtime-benchmarks")]
260		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
261	}
262
263	/// State of pool stakers.
264	#[pallet::storage]
265	pub type PoolStakers<T: Config> = StorageDoubleMap<
266		_,
267		Blake2_128Concat,
268		PoolId,
269		Blake2_128Concat,
270		T::AccountId,
271		PoolStakerInfo<T::Balance>,
272	>;
273
274	/// State and configuration of each staking pool.
275	#[pallet::storage]
276	pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
277
278	/// The cost associated with storing pool information on-chain which was incurred by the pool
279	/// creator.
280	///
281	/// This cost may be [`None`], as determined by [`Config::Consideration`].
282	#[pallet::storage]
283	pub type PoolCost<T: Config> =
284		StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
285
286	/// Stores the [`PoolId`] to use for the next pool.
287	///
288	/// Incremented when a new pool is created.
289	#[pallet::storage]
290	pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
291
292	#[pallet::event]
293	#[pallet::generate_deposit(pub(super) fn deposit_event)]
294	pub enum Event<T: Config> {
295		/// An account staked some tokens in a pool.
296		Staked {
297			/// The account that staked assets.
298			staker: T::AccountId,
299			/// The pool.
300			pool_id: PoolId,
301			/// The staked asset amount.
302			amount: T::Balance,
303		},
304		/// An account unstaked some tokens from a pool.
305		Unstaked {
306			/// The account that signed transaction.
307			caller: T::AccountId,
308			/// The account that unstaked assets.
309			staker: T::AccountId,
310			/// The pool.
311			pool_id: PoolId,
312			/// The unstaked asset amount.
313			amount: T::Balance,
314		},
315		/// An account harvested some rewards.
316		RewardsHarvested {
317			/// The account that signed transaction.
318			caller: T::AccountId,
319			/// The staker whos rewards were harvested.
320			staker: T::AccountId,
321			/// The pool.
322			pool_id: PoolId,
323			/// The amount of harvested tokens.
324			amount: T::Balance,
325		},
326		/// A new reward pool was created.
327		PoolCreated {
328			/// The account that created the pool.
329			creator: T::AccountId,
330			/// The unique ID for the new pool.
331			pool_id: PoolId,
332			/// The staking asset.
333			staked_asset_id: T::AssetId,
334			/// The reward asset.
335			reward_asset_id: T::AssetId,
336			/// The initial reward rate per block.
337			reward_rate_per_block: T::Balance,
338			/// The block the pool will cease to accumulate rewards.
339			expiry_block: BlockNumberFor<T>,
340			/// The account allowed to modify the pool.
341			admin: T::AccountId,
342		},
343		/// A pool reward rate was modified by the admin.
344		PoolRewardRateModified {
345			/// The modified pool.
346			pool_id: PoolId,
347			/// The new reward rate per block.
348			new_reward_rate_per_block: T::Balance,
349		},
350		/// A pool admin was modified.
351		PoolAdminModified {
352			/// The modified pool.
353			pool_id: PoolId,
354			/// The new admin.
355			new_admin: T::AccountId,
356		},
357		/// A pool expiry block was modified by the admin.
358		PoolExpiryBlockModified {
359			/// The modified pool.
360			pool_id: PoolId,
361			/// The new expiry block.
362			new_expiry_block: BlockNumberFor<T>,
363		},
364		/// A pool information was cleared after it's completion.
365		PoolCleanedUp {
366			/// The cleared pool.
367			pool_id: PoolId,
368		},
369	}
370
371	#[pallet::error]
372	pub enum Error<T> {
373		/// The staker does not have enough tokens to perform the operation.
374		NotEnoughTokens,
375		/// An operation was attempted on a non-existent pool.
376		NonExistentPool,
377		/// An operation was attempted for a non-existent staker.
378		NonExistentStaker,
379		/// An operation was attempted with a non-existent asset.
380		NonExistentAsset,
381		/// There was an error converting a block number.
382		BlockNumberConversionError,
383		/// The expiry block must be in the future.
384		ExpiryBlockMustBeInTheFuture,
385		/// Insufficient funds to create the freeze.
386		InsufficientFunds,
387		/// The expiry block can be only extended.
388		ExpiryCut,
389		/// The reward rate per block can be only increased.
390		RewardRateCut,
391		/// The pool still has staked tokens or rewards.
392		NonEmptyPool,
393	}
394
395	#[pallet::hooks]
396	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
397		fn integrity_test() {
398			// The AccountId is at least 16 bytes to contain the unique PalletId.
399			let pool_id: PoolId = 1;
400			assert!(
401				<frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
402					&T::PalletId::get(), pool_id,
403				)
404				.is_some()
405			);
406		}
407	}
408
409	/// Pallet's callable functions.
410	#[pallet::call(weight(<T as Config>::WeightInfo))]
411	impl<T: Config> Pallet<T> {
412		/// Create a new reward pool.
413		///
414		/// Parameters:
415		/// - `origin`: must be `Config::CreatePoolOrigin`;
416		/// - `staked_asset_id`: the asset to be staked in the pool;
417		/// - `reward_asset_id`: the asset to be distributed as rewards;
418		/// - `reward_rate_per_block`: the amount of reward tokens distributed per block;
419		/// - `expiry`: the block number at which the pool will cease to accumulate rewards. The
420		///   [`DispatchTime::After`] variant evaluated at the execution time.
421		/// - `admin`: the account allowed to extend the pool expiration, increase the rewards rate
422		///   and receive the unutilized reward tokens back after the pool completion. If `None`,
423		///   the caller is set as an admin.
424		#[pallet::call_index(0)]
425		pub fn create_pool(
426			origin: OriginFor<T>,
427			staked_asset_id: Box<T::AssetId>,
428			reward_asset_id: Box<T::AssetId>,
429			reward_rate_per_block: T::Balance,
430			expiry: DispatchTime<BlockNumberFor<T>>,
431			admin: Option<T::AccountId>,
432		) -> DispatchResult {
433			// Check the origin.
434			let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
435
436			// Ensure the assets exist.
437			ensure!(
438				T::Assets::asset_exists(*staked_asset_id.clone()),
439				Error::<T>::NonExistentAsset
440			);
441			ensure!(
442				T::Assets::asset_exists(*reward_asset_id.clone()),
443				Error::<T>::NonExistentAsset
444			);
445
446			// Check the expiry block.
447			let expiry_block = expiry.evaluate(frame_system::Pallet::<T>::block_number());
448			ensure!(
449				expiry_block > frame_system::Pallet::<T>::block_number(),
450				Error::<T>::ExpiryBlockMustBeInTheFuture
451			);
452
453			let pool_id = NextPoolId::<T>::get();
454
455			let footprint = Self::pool_creation_footprint();
456			let cost = T::Consideration::new(&creator, footprint)?;
457			PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
458
459			let admin = admin.unwrap_or(creator.clone());
460
461			// Create the pool.
462			let pool = PoolInfoFor::<T> {
463				staked_asset_id: *staked_asset_id.clone(),
464				reward_asset_id: *reward_asset_id.clone(),
465				reward_rate_per_block,
466				total_tokens_staked: 0u32.into(),
467				reward_per_token_stored: 0u32.into(),
468				last_update_block: 0u32.into(),
469				expiry_block,
470				admin: admin.clone(),
471				account: Self::pool_account_id(&pool_id),
472			};
473
474			// Insert it into storage.
475			Pools::<T>::insert(pool_id, pool);
476
477			NextPoolId::<T>::put(pool_id.ensure_add(1)?);
478
479			// Emit created event.
480			Self::deposit_event(Event::PoolCreated {
481				creator,
482				pool_id,
483				staked_asset_id: *staked_asset_id,
484				reward_asset_id: *reward_asset_id,
485				reward_rate_per_block,
486				expiry_block,
487				admin,
488			});
489
490			Ok(())
491		}
492
493		/// Stake additional tokens in a pool.
494		///
495		/// A freeze is placed on the staked tokens.
496		#[pallet::call_index(1)]
497		pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
498			let staker = ensure_signed(origin)?;
499
500			// Always start by updating staker and pool rewards.
501			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
502			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
503			let (mut pool_info, mut staker_info) =
504				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
505
506			T::AssetsFreezer::increase_frozen(
507				pool_info.staked_asset_id.clone(),
508				&FreezeReason::Staked.into(),
509				&staker,
510				amount,
511			)?;
512
513			// Update Pools.
514			pool_info.total_tokens_staked.ensure_add_assign(amount)?;
515
516			Pools::<T>::insert(pool_id, pool_info);
517
518			// Update PoolStakers.
519			staker_info.amount.ensure_add_assign(amount)?;
520			PoolStakers::<T>::insert(pool_id, &staker, staker_info);
521
522			// Emit event.
523			Self::deposit_event(Event::Staked { staker, pool_id, amount });
524
525			Ok(())
526		}
527
528		/// Unstake tokens from a pool.
529		///
530		/// Removes the freeze on the staked tokens.
531		///
532		/// Parameters:
533		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
534		/// - pool_id: the pool to unstake from.
535		/// - amount: the amount of tokens to unstake.
536		/// - staker: the account to unstake from. If `None`, the caller is used.
537		#[pallet::call_index(2)]
538		pub fn unstake(
539			origin: OriginFor<T>,
540			pool_id: PoolId,
541			amount: T::Balance,
542			staker: Option<T::AccountId>,
543		) -> DispatchResult {
544			let caller = ensure_signed(origin)?;
545			let staker = staker.unwrap_or(caller.clone());
546
547			// Always start by updating the pool rewards.
548			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
549			let now = frame_system::Pallet::<T>::block_number();
550			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
551
552			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
553			let (mut pool_info, mut staker_info) =
554				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
555
556			// Check the staker has enough staked tokens.
557			ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
558
559			// Unfreeze staker assets.
560			T::AssetsFreezer::decrease_frozen(
561				pool_info.staked_asset_id.clone(),
562				&FreezeReason::Staked.into(),
563				&staker,
564				amount,
565			)?;
566
567			// Update Pools.
568			pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
569			Pools::<T>::insert(pool_id, pool_info);
570
571			// Update PoolStakers.
572			staker_info.amount.ensure_sub_assign(amount)?;
573
574			if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
575				PoolStakers::<T>::remove(&pool_id, &staker);
576			} else {
577				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
578			}
579
580			// Emit event.
581			Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
582
583			Ok(())
584		}
585
586		/// Harvest unclaimed pool rewards.
587		///
588		/// Parameters:
589		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
590		/// - pool_id: the pool to harvest from.
591		/// - staker: the account for which to harvest rewards. If `None`, the caller is used.
592		#[pallet::call_index(3)]
593		pub fn harvest_rewards(
594			origin: OriginFor<T>,
595			pool_id: PoolId,
596			staker: Option<T::AccountId>,
597		) -> DispatchResult {
598			let caller = ensure_signed(origin)?;
599			let staker = staker.unwrap_or(caller.clone());
600
601			// Always start by updating the pool and staker rewards.
602			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
603			let now = frame_system::Pallet::<T>::block_number();
604			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
605
606			let staker_info =
607				PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
608			let (pool_info, mut staker_info) =
609				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
610
611			// Transfer unclaimed rewards from the pool to the staker.
612			T::Assets::transfer(
613				pool_info.reward_asset_id,
614				&pool_info.account,
615				&staker,
616				staker_info.rewards,
617				// Could kill the account, but only if the pool was already almost empty.
618				Preservation::Expendable,
619			)?;
620
621			// Emit event.
622			Self::deposit_event(Event::RewardsHarvested {
623				caller,
624				staker: staker.clone(),
625				pool_id,
626				amount: staker_info.rewards,
627			});
628
629			// Reset staker rewards.
630			staker_info.rewards = 0u32.into();
631
632			if staker_info.amount.is_zero() {
633				PoolStakers::<T>::remove(&pool_id, &staker);
634			} else {
635				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
636			}
637
638			Ok(())
639		}
640
641		/// Modify a pool reward rate.
642		///
643		/// Currently the reward rate can only be increased.
644		///
645		/// Only the pool admin may perform this operation.
646		#[pallet::call_index(4)]
647		pub fn set_pool_reward_rate_per_block(
648			origin: OriginFor<T>,
649			pool_id: PoolId,
650			new_reward_rate_per_block: T::Balance,
651		) -> DispatchResult {
652			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
653				.or_else(|_| ensure_signed(origin))?;
654
655			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
656			ensure!(pool_info.admin == caller, BadOrigin);
657			ensure!(
658				new_reward_rate_per_block > pool_info.reward_rate_per_block,
659				Error::<T>::RewardRateCut
660			);
661
662			// Always start by updating the pool rewards.
663			let rewards_per_token = Self::reward_per_token(&pool_info)?;
664			let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
665
666			pool_info.reward_rate_per_block = new_reward_rate_per_block;
667			Pools::<T>::insert(pool_id, pool_info);
668
669			Self::deposit_event(Event::PoolRewardRateModified {
670				pool_id,
671				new_reward_rate_per_block,
672			});
673
674			Ok(())
675		}
676
677		/// Modify a pool admin.
678		///
679		/// Only the pool admin may perform this operation.
680		#[pallet::call_index(5)]
681		pub fn set_pool_admin(
682			origin: OriginFor<T>,
683			pool_id: PoolId,
684			new_admin: T::AccountId,
685		) -> DispatchResult {
686			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
687				.or_else(|_| ensure_signed(origin))?;
688
689			let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
690			ensure!(pool_info.admin == caller, BadOrigin);
691
692			pool_info.admin = new_admin.clone();
693			Pools::<T>::insert(pool_id, pool_info);
694
695			Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
696
697			Ok(())
698		}
699
700		/// Set when the pool should expire.
701		///
702		/// Currently the expiry block can only be extended.
703		///
704		/// Only the pool admin may perform this operation.
705		#[pallet::call_index(6)]
706		pub fn set_pool_expiry_block(
707			origin: OriginFor<T>,
708			pool_id: PoolId,
709			new_expiry: DispatchTime<BlockNumberFor<T>>,
710		) -> DispatchResult {
711			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
712				.or_else(|_| ensure_signed(origin))?;
713
714			let new_expiry = new_expiry.evaluate(frame_system::Pallet::<T>::block_number());
715			ensure!(
716				new_expiry > frame_system::Pallet::<T>::block_number(),
717				Error::<T>::ExpiryBlockMustBeInTheFuture
718			);
719
720			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
721			ensure!(pool_info.admin == caller, BadOrigin);
722			ensure!(new_expiry > pool_info.expiry_block, Error::<T>::ExpiryCut);
723
724			// Always start by updating the pool rewards.
725			let reward_per_token = Self::reward_per_token(&pool_info)?;
726			let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
727
728			pool_info.expiry_block = new_expiry;
729			Pools::<T>::insert(pool_id, pool_info);
730
731			Self::deposit_event(Event::PoolExpiryBlockModified {
732				pool_id,
733				new_expiry_block: new_expiry,
734			});
735
736			Ok(())
737		}
738
739		/// Convenience method to deposit reward tokens into a pool.
740		///
741		/// This method is not strictly necessary (tokens could be transferred directly to the
742		/// pool pot address), but is provided for convenience so manual derivation of the
743		/// account id is not required.
744		#[pallet::call_index(7)]
745		pub fn deposit_reward_tokens(
746			origin: OriginFor<T>,
747			pool_id: PoolId,
748			amount: T::Balance,
749		) -> DispatchResult {
750			let caller = ensure_signed(origin)?;
751			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
752			T::Assets::transfer(
753				pool_info.reward_asset_id,
754				&caller,
755				&pool_info.account,
756				amount,
757				Preservation::Preserve,
758			)?;
759			Ok(())
760		}
761
762		/// Cleanup a pool.
763		///
764		/// Origin must be the pool admin.
765		///
766		/// Cleanup storage, release any associated storage cost and return the remaining reward
767		/// tokens to the admin.
768		#[pallet::call_index(8)]
769		pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
770			let who = ensure_signed(origin)?;
771
772			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
773			ensure!(pool_info.admin == who, BadOrigin);
774
775			let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
776			ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
777
778			let pool_balance = T::Assets::reducible_balance(
779				pool_info.reward_asset_id.clone(),
780				&pool_info.account,
781				Preservation::Expendable,
782				Fortitude::Polite,
783			);
784			T::Assets::transfer(
785				pool_info.reward_asset_id,
786				&pool_info.account,
787				&pool_info.admin,
788				pool_balance,
789				Preservation::Expendable,
790			)?;
791
792			if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
793				T::Consideration::drop(cost, &who)?;
794			}
795
796			Pools::<T>::remove(pool_id);
797
798			Self::deposit_event(Event::PoolCleanedUp { pool_id });
799
800			Ok(())
801		}
802	}
803
804	impl<T: Config> Pallet<T> {
805		/// The pool creation footprint.
806		///
807		/// The footprint specifically accounts for the storage footprint of the pool's information
808		/// itself, excluding any potential storage footprint related to the stakers.
809		pub fn pool_creation_footprint() -> Footprint {
810			Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
811		}
812
813		/// Derive a pool account ID from the pool's ID.
814		pub fn pool_account_id(id: &PoolId) -> T::AccountId {
815			T::PalletId::get().into_sub_account_truncating(id)
816		}
817
818		/// Computes update pool and staker reward state.
819		///
820		/// Should be called prior to any operation involving a staker.
821		///
822		/// Returns the updated pool and staker info.
823		///
824		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
825		/// the responsibility of the caller.
826		pub fn update_pool_and_staker_rewards(
827			pool_info: &PoolInfoFor<T>,
828			staker_info: &PoolStakerInfo<T::Balance>,
829		) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
830			let reward_per_token = Self::reward_per_token(&pool_info)?;
831			let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
832
833			let mut new_staker_info = staker_info.clone();
834			new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
835			new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
836			return Ok((pool_info, new_staker_info));
837		}
838
839		/// Computes update pool reward state.
840		///
841		/// Should be called every time the pool is adjusted, and a staker is not involved.
842		///
843		/// Returns the updated pool and staker info.
844		///
845		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
846		/// the responsibility of the caller.
847		pub fn update_pool_rewards(
848			pool_info: &PoolInfoFor<T>,
849			reward_per_token: T::Balance,
850		) -> Result<PoolInfoFor<T>, DispatchError> {
851			let mut new_pool_info = pool_info.clone();
852			new_pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
853			new_pool_info.reward_per_token_stored = reward_per_token;
854
855			Ok(new_pool_info)
856		}
857
858		/// Derives the current reward per token for this pool.
859		fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
860			if pool_info.total_tokens_staked.is_zero() {
861				return Ok(pool_info.reward_per_token_stored)
862			}
863
864			let rewardable_blocks_elapsed: u32 =
865				match Self::last_block_reward_applicable(pool_info.expiry_block)
866					.ensure_sub(pool_info.last_update_block)?
867					.try_into()
868				{
869					Ok(b) => b,
870					Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
871				};
872
873			Ok(pool_info.reward_per_token_stored.ensure_add(
874				pool_info
875					.reward_rate_per_block
876					.ensure_mul(rewardable_blocks_elapsed.into())?
877					.ensure_mul(PRECISION_SCALING_FACTOR.into())?
878					.ensure_div(pool_info.total_tokens_staked)?,
879			)?)
880		}
881
882		/// Derives the amount of rewards earned by a staker.
883		///
884		/// This is a helper function for `update_pool_rewards` and should not be called directly.
885		fn derive_rewards(
886			staker_info: &PoolStakerInfo<T::Balance>,
887			reward_per_token: &T::Balance,
888		) -> Result<T::Balance, DispatchError> {
889			Ok(staker_info
890				.amount
891				.ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
892				.ensure_div(PRECISION_SCALING_FACTOR.into())?
893				.ensure_add(staker_info.rewards)?)
894		}
895
896		fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
897			let now = frame_system::Pallet::<T>::block_number();
898			if now < pool_expiry_block {
899				now
900			} else {
901				pool_expiry_block
902			}
903		}
904	}
905}