referrerpolicy=no-referrer-when-downgrade

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		#[allow(deprecated)]
214		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
215
216		/// The pallet's unique identifier, used to derive the pool's account ID.
217		///
218		/// The account ID is derived once during pool creation and stored in the storage.
219		#[pallet::constant]
220		type PalletId: Get<PalletId>;
221
222		/// Identifier for each type of asset.
223		type AssetId: AssetId + Member + Parameter;
224
225		/// The type in which the assets are measured.
226		type Balance: Balance + TypeInfo;
227
228		/// The origin with permission to create pools.
229		///
230		/// The Origin must return an AccountId.
231		type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
232
233		/// Registry of assets that can be configured to either stake for rewards, or be offered as
234		/// rewards for staking.
235		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
236			+ Mutate<Self::AccountId>;
237
238		/// Freezer for the Assets.
239		type AssetsFreezer: MutateFreeze<
240			Self::AccountId,
241			Id = Self::RuntimeFreezeReason,
242			AssetId = Self::AssetId,
243			Balance = Self::Balance,
244		>;
245
246		/// The overarching freeze reason.
247		type RuntimeFreezeReason: From<FreezeReason>;
248
249		/// Means for associating a cost with the on-chain storage of pool information, which
250		/// is incurred by the pool creator.
251		///
252		/// The passed `Footprint` specifically accounts for the storage footprint of the pool's
253		/// information itself, excluding any potential storage footprint related to the stakers.
254		type Consideration: Consideration<Self::AccountId, Footprint>;
255
256		/// Weight information for extrinsics in this pallet.
257		type WeightInfo: WeightInfo;
258
259		/// Helper for benchmarking.
260		#[cfg(feature = "runtime-benchmarks")]
261		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
262	}
263
264	/// State of pool stakers.
265	#[pallet::storage]
266	pub type PoolStakers<T: Config> = StorageDoubleMap<
267		_,
268		Blake2_128Concat,
269		PoolId,
270		Blake2_128Concat,
271		T::AccountId,
272		PoolStakerInfo<T::Balance>,
273	>;
274
275	/// State and configuration of each staking pool.
276	#[pallet::storage]
277	pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
278
279	/// The cost associated with storing pool information on-chain which was incurred by the pool
280	/// creator.
281	///
282	/// This cost may be [`None`], as determined by [`Config::Consideration`].
283	#[pallet::storage]
284	pub type PoolCost<T: Config> =
285		StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
286
287	/// Stores the [`PoolId`] to use for the next pool.
288	///
289	/// Incremented when a new pool is created.
290	#[pallet::storage]
291	pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
292
293	#[pallet::event]
294	#[pallet::generate_deposit(pub(super) fn deposit_event)]
295	pub enum Event<T: Config> {
296		/// An account staked some tokens in a pool.
297		Staked {
298			/// The account that staked assets.
299			staker: T::AccountId,
300			/// The pool.
301			pool_id: PoolId,
302			/// The staked asset amount.
303			amount: T::Balance,
304		},
305		/// An account unstaked some tokens from a pool.
306		Unstaked {
307			/// The account that signed transaction.
308			caller: T::AccountId,
309			/// The account that unstaked assets.
310			staker: T::AccountId,
311			/// The pool.
312			pool_id: PoolId,
313			/// The unstaked asset amount.
314			amount: T::Balance,
315		},
316		/// An account harvested some rewards.
317		RewardsHarvested {
318			/// The account that signed transaction.
319			caller: T::AccountId,
320			/// The staker whos rewards were harvested.
321			staker: T::AccountId,
322			/// The pool.
323			pool_id: PoolId,
324			/// The amount of harvested tokens.
325			amount: T::Balance,
326		},
327		/// A new reward pool was created.
328		PoolCreated {
329			/// The account that created the pool.
330			creator: T::AccountId,
331			/// The unique ID for the new pool.
332			pool_id: PoolId,
333			/// The staking asset.
334			staked_asset_id: T::AssetId,
335			/// The reward asset.
336			reward_asset_id: T::AssetId,
337			/// The initial reward rate per block.
338			reward_rate_per_block: T::Balance,
339			/// The block the pool will cease to accumulate rewards.
340			expiry_block: BlockNumberFor<T>,
341			/// The account allowed to modify the pool.
342			admin: T::AccountId,
343		},
344		/// A pool reward rate was modified by the admin.
345		PoolRewardRateModified {
346			/// The modified pool.
347			pool_id: PoolId,
348			/// The new reward rate per block.
349			new_reward_rate_per_block: T::Balance,
350		},
351		/// A pool admin was modified.
352		PoolAdminModified {
353			/// The modified pool.
354			pool_id: PoolId,
355			/// The new admin.
356			new_admin: T::AccountId,
357		},
358		/// A pool expiry block was modified by the admin.
359		PoolExpiryBlockModified {
360			/// The modified pool.
361			pool_id: PoolId,
362			/// The new expiry block.
363			new_expiry_block: BlockNumberFor<T>,
364		},
365		/// A pool information was cleared after it's completion.
366		PoolCleanedUp {
367			/// The cleared pool.
368			pool_id: PoolId,
369		},
370	}
371
372	#[pallet::error]
373	pub enum Error<T> {
374		/// The staker does not have enough tokens to perform the operation.
375		NotEnoughTokens,
376		/// An operation was attempted on a non-existent pool.
377		NonExistentPool,
378		/// An operation was attempted for a non-existent staker.
379		NonExistentStaker,
380		/// An operation was attempted with a non-existent asset.
381		NonExistentAsset,
382		/// There was an error converting a block number.
383		BlockNumberConversionError,
384		/// The expiry block must be in the future.
385		ExpiryBlockMustBeInTheFuture,
386		/// Insufficient funds to create the freeze.
387		InsufficientFunds,
388		/// The expiry block can be only extended.
389		ExpiryCut,
390		/// The reward rate per block can be only increased.
391		RewardRateCut,
392		/// The pool still has staked tokens or rewards.
393		NonEmptyPool,
394	}
395
396	#[pallet::hooks]
397	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
398		fn integrity_test() {
399			// The AccountId is at least 16 bytes to contain the unique PalletId.
400			let pool_id: PoolId = 1;
401			assert!(
402				<frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
403					&T::PalletId::get(), pool_id,
404				)
405				.is_some()
406			);
407		}
408	}
409
410	/// Pallet's callable functions.
411	#[pallet::call(weight(<T as Config>::WeightInfo))]
412	impl<T: Config> Pallet<T> {
413		/// Create a new reward pool.
414		///
415		/// Parameters:
416		/// - `origin`: must be `Config::CreatePoolOrigin`;
417		/// - `staked_asset_id`: the asset to be staked in the pool;
418		/// - `reward_asset_id`: the asset to be distributed as rewards;
419		/// - `reward_rate_per_block`: the amount of reward tokens distributed per block;
420		/// - `expiry`: the block number at which the pool will cease to accumulate rewards. The
421		///   [`DispatchTime::After`] variant evaluated at the execution time.
422		/// - `admin`: the account allowed to extend the pool expiration, increase the rewards rate
423		///   and receive the unutilized reward tokens back after the pool completion. If `None`,
424		///   the caller is set as an admin.
425		#[pallet::call_index(0)]
426		pub fn create_pool(
427			origin: OriginFor<T>,
428			staked_asset_id: Box<T::AssetId>,
429			reward_asset_id: Box<T::AssetId>,
430			reward_rate_per_block: T::Balance,
431			expiry: DispatchTime<BlockNumberFor<T>>,
432			admin: Option<T::AccountId>,
433		) -> DispatchResult {
434			// Check the origin.
435			let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
436
437			// Ensure the assets exist.
438			ensure!(
439				T::Assets::asset_exists(*staked_asset_id.clone()),
440				Error::<T>::NonExistentAsset
441			);
442			ensure!(
443				T::Assets::asset_exists(*reward_asset_id.clone()),
444				Error::<T>::NonExistentAsset
445			);
446
447			// Check the expiry block.
448			let expiry_block = expiry.evaluate(frame_system::Pallet::<T>::block_number());
449			ensure!(
450				expiry_block > frame_system::Pallet::<T>::block_number(),
451				Error::<T>::ExpiryBlockMustBeInTheFuture
452			);
453
454			let pool_id = NextPoolId::<T>::get();
455
456			let footprint = Self::pool_creation_footprint();
457			let cost = T::Consideration::new(&creator, footprint)?;
458			PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
459
460			let admin = admin.unwrap_or(creator.clone());
461
462			// Create the pool.
463			let pool = PoolInfoFor::<T> {
464				staked_asset_id: *staked_asset_id.clone(),
465				reward_asset_id: *reward_asset_id.clone(),
466				reward_rate_per_block,
467				total_tokens_staked: 0u32.into(),
468				reward_per_token_stored: 0u32.into(),
469				last_update_block: 0u32.into(),
470				expiry_block,
471				admin: admin.clone(),
472				account: Self::pool_account_id(&pool_id),
473			};
474
475			// Insert it into storage.
476			Pools::<T>::insert(pool_id, pool);
477
478			NextPoolId::<T>::put(pool_id.ensure_add(1)?);
479
480			// Emit created event.
481			Self::deposit_event(Event::PoolCreated {
482				creator,
483				pool_id,
484				staked_asset_id: *staked_asset_id,
485				reward_asset_id: *reward_asset_id,
486				reward_rate_per_block,
487				expiry_block,
488				admin,
489			});
490
491			Ok(())
492		}
493
494		/// Stake additional tokens in a pool.
495		///
496		/// A freeze is placed on the staked tokens.
497		#[pallet::call_index(1)]
498		pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
499			let staker = ensure_signed(origin)?;
500
501			// Always start by updating staker and pool rewards.
502			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
503			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
504			let (mut pool_info, mut staker_info) =
505				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
506
507			T::AssetsFreezer::increase_frozen(
508				pool_info.staked_asset_id.clone(),
509				&FreezeReason::Staked.into(),
510				&staker,
511				amount,
512			)?;
513
514			// Update Pools.
515			pool_info.total_tokens_staked.ensure_add_assign(amount)?;
516
517			Pools::<T>::insert(pool_id, pool_info);
518
519			// Update PoolStakers.
520			staker_info.amount.ensure_add_assign(amount)?;
521			PoolStakers::<T>::insert(pool_id, &staker, staker_info);
522
523			// Emit event.
524			Self::deposit_event(Event::Staked { staker, pool_id, amount });
525
526			Ok(())
527		}
528
529		/// Unstake tokens from a pool.
530		///
531		/// Removes the freeze on the staked tokens.
532		///
533		/// Parameters:
534		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
535		/// - pool_id: the pool to unstake from.
536		/// - amount: the amount of tokens to unstake.
537		/// - staker: the account to unstake from. If `None`, the caller is used.
538		#[pallet::call_index(2)]
539		pub fn unstake(
540			origin: OriginFor<T>,
541			pool_id: PoolId,
542			amount: T::Balance,
543			staker: Option<T::AccountId>,
544		) -> DispatchResult {
545			let caller = ensure_signed(origin)?;
546			let staker = staker.unwrap_or(caller.clone());
547
548			// Always start by updating the pool rewards.
549			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
550			let now = frame_system::Pallet::<T>::block_number();
551			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
552
553			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
554			let (mut pool_info, mut staker_info) =
555				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
556
557			// Check the staker has enough staked tokens.
558			ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
559
560			// Unfreeze staker assets.
561			T::AssetsFreezer::decrease_frozen(
562				pool_info.staked_asset_id.clone(),
563				&FreezeReason::Staked.into(),
564				&staker,
565				amount,
566			)?;
567
568			// Update Pools.
569			pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
570			Pools::<T>::insert(pool_id, pool_info);
571
572			// Update PoolStakers.
573			staker_info.amount.ensure_sub_assign(amount)?;
574
575			if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
576				PoolStakers::<T>::remove(&pool_id, &staker);
577			} else {
578				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
579			}
580
581			// Emit event.
582			Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
583
584			Ok(())
585		}
586
587		/// Harvest unclaimed pool rewards.
588		///
589		/// Parameters:
590		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
591		/// - pool_id: the pool to harvest from.
592		/// - staker: the account for which to harvest rewards. If `None`, the caller is used.
593		#[pallet::call_index(3)]
594		pub fn harvest_rewards(
595			origin: OriginFor<T>,
596			pool_id: PoolId,
597			staker: Option<T::AccountId>,
598		) -> DispatchResult {
599			let caller = ensure_signed(origin)?;
600			let staker = staker.unwrap_or(caller.clone());
601
602			// Always start by updating the pool and staker rewards.
603			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
604			let now = frame_system::Pallet::<T>::block_number();
605			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
606
607			let staker_info =
608				PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
609			let (pool_info, mut staker_info) =
610				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
611
612			// Transfer unclaimed rewards from the pool to the staker.
613			T::Assets::transfer(
614				pool_info.reward_asset_id,
615				&pool_info.account,
616				&staker,
617				staker_info.rewards,
618				// Could kill the account, but only if the pool was already almost empty.
619				Preservation::Expendable,
620			)?;
621
622			// Emit event.
623			Self::deposit_event(Event::RewardsHarvested {
624				caller,
625				staker: staker.clone(),
626				pool_id,
627				amount: staker_info.rewards,
628			});
629
630			// Reset staker rewards.
631			staker_info.rewards = 0u32.into();
632
633			if staker_info.amount.is_zero() {
634				PoolStakers::<T>::remove(&pool_id, &staker);
635			} else {
636				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
637			}
638
639			Ok(())
640		}
641
642		/// Modify a pool reward rate.
643		///
644		/// Currently the reward rate can only be increased.
645		///
646		/// Only the pool admin may perform this operation.
647		#[pallet::call_index(4)]
648		pub fn set_pool_reward_rate_per_block(
649			origin: OriginFor<T>,
650			pool_id: PoolId,
651			new_reward_rate_per_block: T::Balance,
652		) -> DispatchResult {
653			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
654				.or_else(|_| ensure_signed(origin))?;
655
656			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
657			ensure!(pool_info.admin == caller, BadOrigin);
658			ensure!(
659				new_reward_rate_per_block > pool_info.reward_rate_per_block,
660				Error::<T>::RewardRateCut
661			);
662
663			// Always start by updating the pool rewards.
664			let rewards_per_token = Self::reward_per_token(&pool_info)?;
665			let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
666
667			pool_info.reward_rate_per_block = new_reward_rate_per_block;
668			Pools::<T>::insert(pool_id, pool_info);
669
670			Self::deposit_event(Event::PoolRewardRateModified {
671				pool_id,
672				new_reward_rate_per_block,
673			});
674
675			Ok(())
676		}
677
678		/// Modify a pool admin.
679		///
680		/// Only the pool admin may perform this operation.
681		#[pallet::call_index(5)]
682		pub fn set_pool_admin(
683			origin: OriginFor<T>,
684			pool_id: PoolId,
685			new_admin: T::AccountId,
686		) -> DispatchResult {
687			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
688				.or_else(|_| ensure_signed(origin))?;
689
690			let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
691			ensure!(pool_info.admin == caller, BadOrigin);
692
693			pool_info.admin = new_admin.clone();
694			Pools::<T>::insert(pool_id, pool_info);
695
696			Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
697
698			Ok(())
699		}
700
701		/// Set when the pool should expire.
702		///
703		/// Currently the expiry block can only be extended.
704		///
705		/// Only the pool admin may perform this operation.
706		#[pallet::call_index(6)]
707		pub fn set_pool_expiry_block(
708			origin: OriginFor<T>,
709			pool_id: PoolId,
710			new_expiry: DispatchTime<BlockNumberFor<T>>,
711		) -> DispatchResult {
712			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
713				.or_else(|_| ensure_signed(origin))?;
714
715			let new_expiry = new_expiry.evaluate(frame_system::Pallet::<T>::block_number());
716			ensure!(
717				new_expiry > frame_system::Pallet::<T>::block_number(),
718				Error::<T>::ExpiryBlockMustBeInTheFuture
719			);
720
721			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
722			ensure!(pool_info.admin == caller, BadOrigin);
723			ensure!(new_expiry > pool_info.expiry_block, Error::<T>::ExpiryCut);
724
725			// Always start by updating the pool rewards.
726			let reward_per_token = Self::reward_per_token(&pool_info)?;
727			let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
728
729			pool_info.expiry_block = new_expiry;
730			Pools::<T>::insert(pool_id, pool_info);
731
732			Self::deposit_event(Event::PoolExpiryBlockModified {
733				pool_id,
734				new_expiry_block: new_expiry,
735			});
736
737			Ok(())
738		}
739
740		/// Convenience method to deposit reward tokens into a pool.
741		///
742		/// This method is not strictly necessary (tokens could be transferred directly to the
743		/// pool pot address), but is provided for convenience so manual derivation of the
744		/// account id is not required.
745		#[pallet::call_index(7)]
746		pub fn deposit_reward_tokens(
747			origin: OriginFor<T>,
748			pool_id: PoolId,
749			amount: T::Balance,
750		) -> DispatchResult {
751			let caller = ensure_signed(origin)?;
752			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
753			T::Assets::transfer(
754				pool_info.reward_asset_id,
755				&caller,
756				&pool_info.account,
757				amount,
758				Preservation::Preserve,
759			)?;
760			Ok(())
761		}
762
763		/// Cleanup a pool.
764		///
765		/// Origin must be the pool admin.
766		///
767		/// Cleanup storage, release any associated storage cost and return the remaining reward
768		/// tokens to the admin.
769		#[pallet::call_index(8)]
770		pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
771			let who = ensure_signed(origin)?;
772
773			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
774			ensure!(pool_info.admin == who, BadOrigin);
775
776			let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
777			ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
778
779			let pool_balance = T::Assets::reducible_balance(
780				pool_info.reward_asset_id.clone(),
781				&pool_info.account,
782				Preservation::Expendable,
783				Fortitude::Polite,
784			);
785			T::Assets::transfer(
786				pool_info.reward_asset_id,
787				&pool_info.account,
788				&pool_info.admin,
789				pool_balance,
790				Preservation::Expendable,
791			)?;
792
793			if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
794				T::Consideration::drop(cost, &who)?;
795			}
796
797			Pools::<T>::remove(pool_id);
798
799			Self::deposit_event(Event::PoolCleanedUp { pool_id });
800
801			Ok(())
802		}
803	}
804
805	impl<T: Config> Pallet<T> {
806		/// The pool creation footprint.
807		///
808		/// The footprint specifically accounts for the storage footprint of the pool's information
809		/// itself, excluding any potential storage footprint related to the stakers.
810		pub fn pool_creation_footprint() -> Footprint {
811			Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
812		}
813
814		/// Derive a pool account ID from the pool's ID.
815		pub fn pool_account_id(id: &PoolId) -> T::AccountId {
816			T::PalletId::get().into_sub_account_truncating(id)
817		}
818
819		/// Computes update pool and staker reward state.
820		///
821		/// Should be called prior to any operation involving a staker.
822		///
823		/// Returns the updated pool and staker info.
824		///
825		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
826		/// the responsibility of the caller.
827		pub fn update_pool_and_staker_rewards(
828			pool_info: &PoolInfoFor<T>,
829			staker_info: &PoolStakerInfo<T::Balance>,
830		) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
831			let reward_per_token = Self::reward_per_token(&pool_info)?;
832			let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
833
834			let mut new_staker_info = staker_info.clone();
835			new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
836			new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
837			return Ok((pool_info, new_staker_info));
838		}
839
840		/// Computes update pool reward state.
841		///
842		/// Should be called every time the pool is adjusted, and a staker is not involved.
843		///
844		/// Returns the updated pool and staker info.
845		///
846		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
847		/// the responsibility of the caller.
848		pub fn update_pool_rewards(
849			pool_info: &PoolInfoFor<T>,
850			reward_per_token: T::Balance,
851		) -> Result<PoolInfoFor<T>, DispatchError> {
852			let mut new_pool_info = pool_info.clone();
853			new_pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
854			new_pool_info.reward_per_token_stored = reward_per_token;
855
856			Ok(new_pool_info)
857		}
858
859		/// Derives the current reward per token for this pool.
860		fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
861			if pool_info.total_tokens_staked.is_zero() {
862				return Ok(pool_info.reward_per_token_stored)
863			}
864
865			let rewardable_blocks_elapsed: u32 =
866				match Self::last_block_reward_applicable(pool_info.expiry_block)
867					.ensure_sub(pool_info.last_update_block)?
868					.try_into()
869				{
870					Ok(b) => b,
871					Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
872				};
873
874			Ok(pool_info.reward_per_token_stored.ensure_add(
875				pool_info
876					.reward_rate_per_block
877					.ensure_mul(rewardable_blocks_elapsed.into())?
878					.ensure_mul(PRECISION_SCALING_FACTOR.into())?
879					.ensure_div(pool_info.total_tokens_staked)?,
880			)?)
881		}
882
883		/// Derives the amount of rewards earned by a staker.
884		///
885		/// This is a helper function for `update_pool_rewards` and should not be called directly.
886		fn derive_rewards(
887			staker_info: &PoolStakerInfo<T::Balance>,
888			reward_per_token: &T::Balance,
889		) -> Result<T::Balance, DispatchError> {
890			Ok(staker_info
891				.amount
892				.ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
893				.ensure_div(PRECISION_SCALING_FACTOR.into())?
894				.ensure_add(staker_info.rewards)?)
895		}
896
897		fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
898			let now = frame_system::Pallet::<T>::block_number();
899			if now < pool_expiry_block {
900				now
901			} else {
902				pool_expiry_block
903			}
904		}
905	}
906}