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	ensure,
88	traits::{
89		fungibles::{Inspect, Mutate},
90		schedule::DispatchTime,
91		tokens::Balance,
92		Consideration, RewardsPool,
93	},
94	PalletId,
95};
96use scale_info::TypeInfo;
97use sp_core::Get;
98use sp_runtime::{
99	traits::{BadOrigin, BlockNumberProvider, EnsureAdd, MaybeDisplay, Zero},
100	DispatchError, DispatchResult,
101};
102use sp_std::boxed::Box;
103
104#[cfg(feature = "runtime-benchmarks")]
105pub mod benchmarking;
106#[cfg(test)]
107mod mock;
108#[cfg(test)]
109mod tests;
110mod weights;
111
112pub use weights::WeightInfo;
113
114/// Unique id type for each pool.
115pub type PoolId = u32;
116
117/// Multiplier to maintain precision when calculating rewards.
118pub(crate) const PRECISION_SCALING_FACTOR: u16 = 4096;
119
120/// Convenience alias for `PoolInfo`.
121pub type PoolInfoFor<T> = PoolInfo<
122	<T as frame_system::Config>::AccountId,
123	<T as Config>::AssetId,
124	<T as Config>::Balance,
125	BlockNumberFor<T>,
126>;
127
128/// The block number type for the pallet.
129///
130/// This type is derived from the `BlockNumberProvider` associated type in the `Config` trait.
131/// It represents the block number type that the pallet uses for scheduling and expiration.
132pub type BlockNumberFor<T> =
133	<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
134
135/// The state of a staker in a pool.
136#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
137pub struct PoolStakerInfo<Balance> {
138	/// Amount of tokens staked.
139	amount: Balance,
140	/// Accumulated, unpaid rewards.
141	rewards: Balance,
142	/// Reward per token value at the time of the staker's last interaction with the contract.
143	reward_per_token_paid: Balance,
144}
145
146/// The state and configuration of an incentive pool.
147#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
148pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
149	/// The asset staked in this pool.
150	staked_asset_id: AssetId,
151	/// The asset distributed as rewards by this pool.
152	reward_asset_id: AssetId,
153	/// The amount of tokens rewarded per block.
154	reward_rate_per_block: Balance,
155	/// The block the pool will cease distributing rewards.
156	expiry_block: BlockNumber,
157	/// The account authorized to manage this pool.
158	admin: AccountId,
159	/// The total amount of tokens staked in this pool.
160	total_tokens_staked: Balance,
161	/// Total rewards accumulated per token, up to the `last_update_block`.
162	reward_per_token_stored: Balance,
163	/// Last block number the pool was updated.
164	last_update_block: BlockNumber,
165	/// The account that holds the pool's rewards.
166	account: AccountId,
167}
168
169sp_api::decl_runtime_apis! {
170	/// The runtime API for the asset rewards pallet.
171	pub trait AssetRewards<Cost: MaybeDisplay + Codec> {
172		/// Get the cost of creating a pool.
173		///
174		/// This is especially useful when the cost is dynamic.
175		fn pool_creation_cost() -> Cost;
176	}
177}
178
179#[frame_support::pallet]
180pub mod pallet {
181	use super::*;
182	use frame_support::{
183		pallet_prelude::*,
184		traits::{
185			fungibles::MutateFreeze,
186			tokens::{AssetId, Fortitude, Preservation},
187			Consideration, Footprint, RewardsPool,
188		},
189	};
190	use frame_system::pallet_prelude::{
191		ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
192	};
193	use sp_runtime::{
194		traits::{
195			AccountIdConversion, BadOrigin, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureMul,
196			EnsureSub, EnsureSubAssign,
197		},
198		DispatchResult,
199	};
200
201	#[pallet::pallet]
202	pub struct Pallet<T>(_);
203
204	/// A reason for the pallet placing a hold on funds.
205	#[pallet::composite_enum]
206	pub enum FreezeReason {
207		/// Funds are staked in the pallet.
208		#[codec(index = 0)]
209		Staked,
210	}
211
212	/// A reason for the pallet placing a hold on funds.
213	#[pallet::composite_enum]
214	pub enum HoldReason {
215		/// Cost associated with storing pool information on-chain.
216		#[codec(index = 0)]
217		PoolCreation,
218	}
219
220	#[pallet::config]
221	pub trait Config: frame_system::Config {
222		/// Overarching event type.
223		#[allow(deprecated)]
224		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
225
226		/// The pallet's unique identifier, used to derive the pool's account ID.
227		///
228		/// The account ID is derived once during pool creation and stored in the storage.
229		#[pallet::constant]
230		type PalletId: Get<PalletId>;
231
232		/// Identifier for each type of asset.
233		type AssetId: AssetId + Member + Parameter;
234
235		/// The type in which the assets are measured.
236		type Balance: Balance + TypeInfo;
237
238		/// The origin with permission to create pools.
239		///
240		/// The Origin must return an AccountId.
241		type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
242
243		/// Registry of assets that can be configured to either stake for rewards, or be offered as
244		/// rewards for staking.
245		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
246			+ Mutate<Self::AccountId>;
247
248		/// Freezer for the Assets.
249		type AssetsFreezer: MutateFreeze<
250			Self::AccountId,
251			Id = Self::RuntimeFreezeReason,
252			AssetId = Self::AssetId,
253			Balance = Self::Balance,
254		>;
255
256		/// The overarching freeze reason.
257		type RuntimeFreezeReason: From<FreezeReason>;
258
259		/// Means for associating a cost with the on-chain storage of pool information, which
260		/// is incurred by the pool creator.
261		///
262		/// The passed `Footprint` specifically accounts for the storage footprint of the pool's
263		/// information itself, excluding any potential storage footprint related to the stakers.
264		type Consideration: Consideration<Self::AccountId, Footprint>;
265
266		/// Weight information for extrinsics in this pallet.
267		type WeightInfo: WeightInfo;
268
269		/// Provider for the current block number.
270		///
271		/// This provider is used to determine the current block number for the pallet.
272		/// It must return monotonically increasing values when called from consecutive blocks.
273		///
274		/// It can be configured to use the local block number (via `frame_system::Pallet`) or a
275		/// remote block number (e.g., from a relay chain). However, note that using a remote
276		/// block number might have implications for the behavior of the pallet, especially if the
277		/// remote block number advances faster than the local block number.
278		///
279		/// It is recommended to use the local block number for solo chains and relay chains.
280		type BlockNumberProvider: BlockNumberProvider;
281
282		/// Helper for benchmarking.
283		#[cfg(feature = "runtime-benchmarks")]
284		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
285	}
286
287	/// State of pool stakers.
288	#[pallet::storage]
289	pub type PoolStakers<T: Config> = StorageDoubleMap<
290		_,
291		Blake2_128Concat,
292		PoolId,
293		Blake2_128Concat,
294		T::AccountId,
295		PoolStakerInfo<T::Balance>,
296	>;
297
298	/// State and configuration of each staking pool.
299	#[pallet::storage]
300	pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
301
302	/// The cost associated with storing pool information on-chain which was incurred by the pool
303	/// creator.
304	///
305	/// This cost may be [`None`], as determined by [`Config::Consideration`].
306	#[pallet::storage]
307	pub type PoolCost<T: Config> =
308		StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
309
310	/// Stores the [`PoolId`] to use for the next pool.
311	///
312	/// Incremented when a new pool is created.
313	#[pallet::storage]
314	pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
315
316	#[pallet::event]
317	#[pallet::generate_deposit(pub(super) fn deposit_event)]
318	pub enum Event<T: Config> {
319		/// An account staked some tokens in a pool.
320		Staked {
321			/// The account that staked assets.
322			staker: T::AccountId,
323			/// The pool.
324			pool_id: PoolId,
325			/// The staked asset amount.
326			amount: T::Balance,
327		},
328		/// An account unstaked some tokens from a pool.
329		Unstaked {
330			/// The account that signed transaction.
331			caller: T::AccountId,
332			/// The account that unstaked assets.
333			staker: T::AccountId,
334			/// The pool.
335			pool_id: PoolId,
336			/// The unstaked asset amount.
337			amount: T::Balance,
338		},
339		/// An account harvested some rewards.
340		RewardsHarvested {
341			/// The account that signed transaction.
342			caller: T::AccountId,
343			/// The staker whos rewards were harvested.
344			staker: T::AccountId,
345			/// The pool.
346			pool_id: PoolId,
347			/// The amount of harvested tokens.
348			amount: T::Balance,
349		},
350		/// A new reward pool was created.
351		PoolCreated {
352			/// The account that created the pool.
353			creator: T::AccountId,
354			/// The unique ID for the new pool.
355			pool_id: PoolId,
356			/// The staking asset.
357			staked_asset_id: T::AssetId,
358			/// The reward asset.
359			reward_asset_id: T::AssetId,
360			/// The initial reward rate per block.
361			reward_rate_per_block: T::Balance,
362			/// The block the pool will cease to accumulate rewards.
363			expiry_block: BlockNumberFor<T>,
364			/// The account allowed to modify the pool.
365			admin: T::AccountId,
366		},
367		/// A pool reward rate was modified by the admin.
368		PoolRewardRateModified {
369			/// The modified pool.
370			pool_id: PoolId,
371			/// The new reward rate per block.
372			new_reward_rate_per_block: T::Balance,
373		},
374		/// A pool admin was modified.
375		PoolAdminModified {
376			/// The modified pool.
377			pool_id: PoolId,
378			/// The new admin.
379			new_admin: T::AccountId,
380		},
381		/// A pool expiry block was modified by the admin.
382		PoolExpiryBlockModified {
383			/// The modified pool.
384			pool_id: PoolId,
385			/// The new expiry block.
386			new_expiry_block: BlockNumberFor<T>,
387		},
388		/// A pool information was cleared after it's completion.
389		PoolCleanedUp {
390			/// The cleared pool.
391			pool_id: PoolId,
392		},
393	}
394
395	#[pallet::error]
396	pub enum Error<T> {
397		/// The staker does not have enough tokens to perform the operation.
398		NotEnoughTokens,
399		/// An operation was attempted on a non-existent pool.
400		NonExistentPool,
401		/// An operation was attempted for a non-existent staker.
402		NonExistentStaker,
403		/// An operation was attempted with a non-existent asset.
404		NonExistentAsset,
405		/// There was an error converting a block number.
406		BlockNumberConversionError,
407		/// The expiry block must be in the future.
408		ExpiryBlockMustBeInTheFuture,
409		/// Insufficient funds to create the freeze.
410		InsufficientFunds,
411		/// The expiry block can be only extended.
412		ExpiryCut,
413		/// The reward rate per block can be only increased.
414		RewardRateCut,
415		/// The pool still has staked tokens or rewards.
416		NonEmptyPool,
417	}
418
419	#[pallet::hooks]
420	impl<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
421		fn integrity_test() {
422			// The AccountId is at least 16 bytes to contain the unique PalletId.
423			let pool_id: PoolId = 1;
424			assert!(
425				<frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
426					&T::PalletId::get(), pool_id,
427				)
428				.is_some()
429			);
430		}
431	}
432
433	/// Pallet's callable functions.
434	#[pallet::call(weight(<T as Config>::WeightInfo))]
435	impl<T: Config> Pallet<T> {
436		/// Create a new reward pool.
437		///
438		/// Parameters:
439		/// - `origin`: must be `Config::CreatePoolOrigin`;
440		/// - `staked_asset_id`: the asset to be staked in the pool;
441		/// - `reward_asset_id`: the asset to be distributed as rewards;
442		/// - `reward_rate_per_block`: the amount of reward tokens distributed per block;
443		/// - `expiry`: the block number at which the pool will cease to accumulate rewards. The
444		///   [`DispatchTime::After`] variant evaluated at the execution time.
445		/// - `admin`: the account allowed to extend the pool expiration, increase the rewards rate
446		///   and receive the unutilized reward tokens back after the pool completion. If `None`,
447		///   the caller is set as an admin.
448		#[pallet::call_index(0)]
449		pub fn create_pool(
450			origin: OriginFor<T>,
451			staked_asset_id: Box<T::AssetId>,
452			reward_asset_id: Box<T::AssetId>,
453			reward_rate_per_block: T::Balance,
454			expiry: DispatchTime<BlockNumberFor<T>>,
455			admin: Option<T::AccountId>,
456		) -> DispatchResult {
457			let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
458			<Self as RewardsPool<_>>::create_pool(
459				&creator,
460				*staked_asset_id,
461				*reward_asset_id,
462				reward_rate_per_block,
463				expiry,
464				&admin.unwrap_or_else(|| creator.clone()),
465			)?;
466			Ok(())
467		}
468
469		/// Stake additional tokens in a pool.
470		///
471		/// A freeze is placed on the staked tokens.
472		#[pallet::call_index(1)]
473		pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
474			let staker = ensure_signed(origin)?;
475
476			// Always start by updating staker and pool rewards.
477			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
478			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
479			let (mut pool_info, mut staker_info) =
480				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
481
482			T::AssetsFreezer::increase_frozen(
483				pool_info.staked_asset_id.clone(),
484				&FreezeReason::Staked.into(),
485				&staker,
486				amount,
487			)?;
488
489			// Update Pools.
490			pool_info.total_tokens_staked.ensure_add_assign(amount)?;
491
492			Pools::<T>::insert(pool_id, pool_info);
493
494			// Update PoolStakers.
495			staker_info.amount.ensure_add_assign(amount)?;
496			PoolStakers::<T>::insert(pool_id, &staker, staker_info);
497
498			// Emit event.
499			Self::deposit_event(Event::Staked { staker, pool_id, amount });
500
501			Ok(())
502		}
503
504		/// Unstake tokens from a pool.
505		///
506		/// Removes the freeze on the staked tokens.
507		///
508		/// Parameters:
509		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
510		/// - pool_id: the pool to unstake from.
511		/// - amount: the amount of tokens to unstake.
512		/// - staker: the account to unstake from. If `None`, the caller is used.
513		#[pallet::call_index(2)]
514		pub fn unstake(
515			origin: OriginFor<T>,
516			pool_id: PoolId,
517			amount: T::Balance,
518			staker: Option<T::AccountId>,
519		) -> DispatchResult {
520			let caller = ensure_signed(origin)?;
521			let staker = staker.unwrap_or(caller.clone());
522
523			// Always start by updating the pool rewards.
524			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
525			let now = T::BlockNumberProvider::current_block_number();
526			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
527
528			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
529			let (mut pool_info, mut staker_info) =
530				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
531
532			// Check the staker has enough staked tokens.
533			ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
534
535			// Unfreeze staker assets.
536			T::AssetsFreezer::decrease_frozen(
537				pool_info.staked_asset_id.clone(),
538				&FreezeReason::Staked.into(),
539				&staker,
540				amount,
541			)?;
542
543			// Update Pools.
544			pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
545			Pools::<T>::insert(pool_id, pool_info);
546
547			// Update PoolStakers.
548			staker_info.amount.ensure_sub_assign(amount)?;
549
550			if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
551				PoolStakers::<T>::remove(&pool_id, &staker);
552			} else {
553				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
554			}
555
556			// Emit event.
557			Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
558
559			Ok(())
560		}
561
562		/// Harvest unclaimed pool rewards.
563		///
564		/// Parameters:
565		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
566		/// - pool_id: the pool to harvest from.
567		/// - staker: the account for which to harvest rewards. If `None`, the caller is used.
568		#[pallet::call_index(3)]
569		pub fn harvest_rewards(
570			origin: OriginFor<T>,
571			pool_id: PoolId,
572			staker: Option<T::AccountId>,
573		) -> DispatchResult {
574			let caller = ensure_signed(origin)?;
575			let staker = staker.unwrap_or(caller.clone());
576
577			// Always start by updating the pool and staker rewards.
578			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
579			let now = T::BlockNumberProvider::current_block_number();
580			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
581
582			let staker_info =
583				PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
584			let (pool_info, mut staker_info) =
585				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
586
587			// Transfer unclaimed rewards from the pool to the staker.
588			T::Assets::transfer(
589				pool_info.reward_asset_id,
590				&pool_info.account,
591				&staker,
592				staker_info.rewards,
593				// Could kill the account, but only if the pool was already almost empty.
594				Preservation::Expendable,
595			)?;
596
597			// Emit event.
598			Self::deposit_event(Event::RewardsHarvested {
599				caller,
600				staker: staker.clone(),
601				pool_id,
602				amount: staker_info.rewards,
603			});
604
605			// Reset staker rewards.
606			staker_info.rewards = 0u32.into();
607
608			if staker_info.amount.is_zero() {
609				PoolStakers::<T>::remove(&pool_id, &staker);
610			} else {
611				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
612			}
613
614			Ok(())
615		}
616
617		/// Modify a pool reward rate.
618		///
619		/// Currently the reward rate can only be increased.
620		///
621		/// Only the pool admin may perform this operation.
622		#[pallet::call_index(4)]
623		pub fn set_pool_reward_rate_per_block(
624			origin: OriginFor<T>,
625			pool_id: PoolId,
626			new_reward_rate_per_block: T::Balance,
627		) -> DispatchResult {
628			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
629				.or_else(|_| ensure_signed(origin))?;
630			<Self as RewardsPool<_>>::set_pool_reward_rate_per_block(
631				&caller,
632				pool_id,
633				new_reward_rate_per_block,
634			)
635		}
636
637		/// Modify a pool admin.
638		///
639		/// Only the pool admin may perform this operation.
640		#[pallet::call_index(5)]
641		pub fn set_pool_admin(
642			origin: OriginFor<T>,
643			pool_id: PoolId,
644			new_admin: T::AccountId,
645		) -> DispatchResult {
646			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
647				.or_else(|_| ensure_signed(origin))?;
648			<Self as RewardsPool<_>>::set_pool_admin(&caller, pool_id, new_admin)
649		}
650
651		/// Set when the pool should expire.
652		///
653		/// Currently the expiry block can only be extended.
654		///
655		/// Only the pool admin may perform this operation.
656		#[pallet::call_index(6)]
657		pub fn set_pool_expiry_block(
658			origin: OriginFor<T>,
659			pool_id: PoolId,
660			new_expiry: DispatchTime<BlockNumberFor<T>>,
661		) -> DispatchResult {
662			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
663				.or_else(|_| ensure_signed(origin))?;
664			<Self as RewardsPool<_>>::set_pool_expiry_block(&caller, pool_id, new_expiry)
665		}
666
667		/// Convenience method to deposit reward tokens into a pool.
668		///
669		/// This method is not strictly necessary (tokens could be transferred directly to the
670		/// pool pot address), but is provided for convenience so manual derivation of the
671		/// account id is not required.
672		#[pallet::call_index(7)]
673		pub fn deposit_reward_tokens(
674			origin: OriginFor<T>,
675			pool_id: PoolId,
676			amount: T::Balance,
677		) -> DispatchResult {
678			let caller = ensure_signed(origin)?;
679			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
680			T::Assets::transfer(
681				pool_info.reward_asset_id,
682				&caller,
683				&pool_info.account,
684				amount,
685				Preservation::Preserve,
686			)?;
687			Ok(())
688		}
689
690		/// Cleanup a pool.
691		///
692		/// Origin must be the pool admin.
693		///
694		/// Cleanup storage, release any associated storage cost and return the remaining reward
695		/// tokens to the admin.
696		#[pallet::call_index(8)]
697		pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
698			let who = ensure_signed(origin)?;
699
700			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
701			ensure!(pool_info.admin == who, BadOrigin);
702
703			let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
704			ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
705
706			let pool_balance = T::Assets::reducible_balance(
707				pool_info.reward_asset_id.clone(),
708				&pool_info.account,
709				Preservation::Expendable,
710				Fortitude::Polite,
711			);
712			T::Assets::transfer(
713				pool_info.reward_asset_id,
714				&pool_info.account,
715				&pool_info.admin,
716				pool_balance,
717				Preservation::Expendable,
718			)?;
719
720			if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
721				T::Consideration::drop(cost, &who)?;
722			}
723
724			Pools::<T>::remove(pool_id);
725
726			Self::deposit_event(Event::PoolCleanedUp { pool_id });
727
728			Ok(())
729		}
730	}
731
732	impl<T: Config> Pallet<T> {
733		/// The pool creation footprint.
734		///
735		/// The footprint specifically accounts for the storage footprint of the pool's information
736		/// itself, excluding any potential storage footprint related to the stakers.
737		pub fn pool_creation_footprint() -> Footprint {
738			Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
739		}
740
741		/// Derive a pool account ID from the pool's ID.
742		pub fn pool_account_id(id: &PoolId) -> T::AccountId {
743			T::PalletId::get().into_sub_account_truncating(id)
744		}
745
746		/// Computes update pool and staker reward state.
747		///
748		/// Should be called prior to any operation involving a staker.
749		///
750		/// Returns the updated pool and staker info.
751		///
752		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
753		/// the responsibility of the caller.
754		pub fn update_pool_and_staker_rewards(
755			pool_info: &PoolInfoFor<T>,
756			staker_info: &PoolStakerInfo<T::Balance>,
757		) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
758			let reward_per_token = Self::reward_per_token(&pool_info)?;
759			let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
760
761			let mut new_staker_info = staker_info.clone();
762			new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
763			new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
764			return Ok((pool_info, new_staker_info));
765		}
766
767		/// Computes update pool reward state.
768		///
769		/// Should be called every time the pool is adjusted, and a staker is not involved.
770		///
771		/// Returns the updated pool and staker info.
772		///
773		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
774		/// the responsibility of the caller.
775		pub fn update_pool_rewards(
776			pool_info: &PoolInfoFor<T>,
777			reward_per_token: T::Balance,
778		) -> Result<PoolInfoFor<T>, DispatchError> {
779			let mut new_pool_info = pool_info.clone();
780			new_pool_info.last_update_block = T::BlockNumberProvider::current_block_number();
781			new_pool_info.reward_per_token_stored = reward_per_token;
782
783			Ok(new_pool_info)
784		}
785
786		/// Derives the current reward per token for this pool.
787		pub(super) fn reward_per_token(
788			pool_info: &PoolInfoFor<T>,
789		) -> Result<T::Balance, DispatchError> {
790			if pool_info.total_tokens_staked.is_zero() {
791				return Ok(pool_info.reward_per_token_stored)
792			}
793
794			let rewardable_blocks_elapsed: u32 =
795				match Self::last_block_reward_applicable(pool_info.expiry_block)
796					.ensure_sub(pool_info.last_update_block)?
797					.try_into()
798				{
799					Ok(b) => b,
800					Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
801				};
802
803			Ok(pool_info.reward_per_token_stored.ensure_add(
804				pool_info
805					.reward_rate_per_block
806					.ensure_mul(rewardable_blocks_elapsed.into())?
807					.ensure_mul(PRECISION_SCALING_FACTOR.into())?
808					.ensure_div(pool_info.total_tokens_staked)?,
809			)?)
810		}
811
812		/// Derives the amount of rewards earned by a staker.
813		///
814		/// This is a helper function for `update_pool_rewards` and should not be called directly.
815		fn derive_rewards(
816			staker_info: &PoolStakerInfo<T::Balance>,
817			reward_per_token: &T::Balance,
818		) -> Result<T::Balance, DispatchError> {
819			Ok(staker_info
820				.amount
821				.ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
822				.ensure_div(PRECISION_SCALING_FACTOR.into())?
823				.ensure_add(staker_info.rewards)?)
824		}
825
826		fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
827			let now = T::BlockNumberProvider::current_block_number();
828			if now < pool_expiry_block {
829				now
830			} else {
831				pool_expiry_block
832			}
833		}
834	}
835}
836
837impl<T: Config> RewardsPool<T::AccountId> for Pallet<T> {
838	type AssetId = T::AssetId;
839	type BlockNumber = BlockNumberFor<T>;
840	type PoolId = PoolId;
841	type Balance = T::Balance;
842
843	fn create_pool(
844		creator: &T::AccountId,
845		staked_asset_id: T::AssetId,
846		reward_asset_id: T::AssetId,
847		reward_rate_per_block: T::Balance,
848		expiry: DispatchTime<BlockNumberFor<T>>,
849		admin: &T::AccountId,
850	) -> Result<PoolId, DispatchError> {
851		// Ensure the assets exist.
852		ensure!(T::Assets::asset_exists(staked_asset_id.clone()), Error::<T>::NonExistentAsset);
853		ensure!(T::Assets::asset_exists(reward_asset_id.clone()), Error::<T>::NonExistentAsset);
854
855		// Check the expiry block.
856		let now = T::BlockNumberProvider::current_block_number();
857		let expiry_block = expiry.evaluate(now);
858		ensure!(expiry_block > now, Error::<T>::ExpiryBlockMustBeInTheFuture);
859
860		let pool_id = NextPoolId::<T>::try_mutate(|id| -> Result<PoolId, DispatchError> {
861			let current_id = *id;
862			*id = id.ensure_add(1)?;
863			Ok(current_id)
864		})?;
865
866		let footprint = Self::pool_creation_footprint();
867		let cost = T::Consideration::new(creator, footprint)?;
868		PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
869
870		// Create the pool.
871		let pool = PoolInfoFor::<T> {
872			staked_asset_id: staked_asset_id.clone(),
873			reward_asset_id: reward_asset_id.clone(),
874			reward_rate_per_block,
875			total_tokens_staked: 0u32.into(),
876			reward_per_token_stored: 0u32.into(),
877			last_update_block: 0u32.into(),
878			expiry_block,
879			admin: admin.clone(),
880			account: Self::pool_account_id(&pool_id),
881		};
882
883		// Insert it into storage.
884		Pools::<T>::insert(pool_id, pool);
885
886		// Emit created event.
887		Self::deposit_event(Event::PoolCreated {
888			creator: creator.clone(),
889			pool_id,
890			staked_asset_id,
891			reward_asset_id,
892			reward_rate_per_block,
893			expiry_block,
894			admin: admin.clone(),
895		});
896
897		Ok(pool_id)
898	}
899
900	fn set_pool_reward_rate_per_block(
901		admin: &T::AccountId,
902		pool_id: PoolId,
903		new_reward_rate_per_block: T::Balance,
904	) -> DispatchResult {
905		let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
906		ensure!(pool_info.admin == *admin, BadOrigin);
907		ensure!(
908			new_reward_rate_per_block > pool_info.reward_rate_per_block,
909			Error::<T>::RewardRateCut
910		);
911
912		// Always start by updating the pool rewards.
913		let rewards_per_token = Self::reward_per_token(&pool_info)?;
914		let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
915
916		pool_info.reward_rate_per_block = new_reward_rate_per_block;
917		Pools::<T>::insert(pool_id, pool_info);
918
919		Self::deposit_event(Event::PoolRewardRateModified { pool_id, new_reward_rate_per_block });
920
921		Ok(())
922	}
923
924	fn set_pool_admin(
925		admin: &T::AccountId,
926		pool_id: PoolId,
927		new_admin: T::AccountId,
928	) -> DispatchResult {
929		let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
930		ensure!(pool_info.admin == *admin, BadOrigin);
931
932		pool_info.admin = new_admin.clone();
933		Pools::<T>::insert(pool_id, pool_info);
934
935		Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
936
937		Ok(())
938	}
939
940	fn set_pool_expiry_block(
941		admin: &T::AccountId,
942		pool_id: PoolId,
943		new_expiry: DispatchTime<BlockNumberFor<T>>,
944	) -> DispatchResult {
945		let now = T::BlockNumberProvider::current_block_number();
946		let new_expiry_block = new_expiry.evaluate(now);
947		ensure!(new_expiry_block > now, Error::<T>::ExpiryBlockMustBeInTheFuture);
948
949		let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
950		ensure!(pool_info.admin == *admin, BadOrigin);
951		ensure!(new_expiry_block > pool_info.expiry_block, Error::<T>::ExpiryCut);
952
953		// Always start by updating the pool rewards.
954		let reward_per_token = Self::reward_per_token(&pool_info)?;
955		let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
956
957		pool_info.expiry_block = new_expiry_block;
958		Pools::<T>::insert(pool_id, pool_info);
959
960		Self::deposit_event(Event::PoolExpiryBlockModified { pool_id, new_expiry_block });
961
962		Ok(())
963	}
964}