Skip to content
Snippets Groups Projects
lib.rs 22 KiB
Newer Older
Liam Aharon's avatar
Liam Aharon committed
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! # FRAME Staking Rewards Pallet
//!
Liam Aharon's avatar
Liam Aharon committed
//! Allows accounts to be rewarded for holding `fungible` asset/s, for example LP tokens.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! ## Overview
//!
Liam Aharon's avatar
Liam Aharon committed
//! A permissioned origin can initiate an incentive program for a fungible asset by creating a new
//! pool.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! During pool creation, a 'staking asset', 'reward asset', 'reward rate per block', 'expiry
//! block', and 'admin' are specified. By default, the permissioned origin is the admin.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! Once created, holders of the 'staking asset' can 'stake' them in a corresponding pool, which
//! creates a Freeze on the asset.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! Once staked, rewards denominated in 'reward asset' begin accumulating to the staker,
Liam Aharon's avatar
Liam Aharon committed
//! proportional to their share of the total staked tokens in the pool.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! Reward assets pending distribution are held in an account unique to each pool.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! Care should be taken by the pool operator to keep pool accounts adequately funded with the
//! reward asset.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! The pool admin may adjust the pool configuration such as reward rate per block, expiry block,
//! and admin.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! ## Permissioning
//!
//! Currently, pool creation and management is permissioned and restricted to a configured Origin.
//!
//! Future iterations of this pallet may allow permissionless creation and management of pools.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! Note: The permissioned origin must return an AccountId. This can be achieved for any Origin by
//! wrapping it with `EnsureSuccess`.
//!
Liam Aharon's avatar
Liam Aharon committed
//! ## Implementation Notes
//!
Liam Aharon's avatar
Liam Aharon committed
//! Internal logic functions such as `update_pool_and_staker_rewards` where deliberately written
Liam Aharon's avatar
Liam Aharon committed
//! without storage interaction.
Liam Aharon's avatar
Liam Aharon committed
//!
//! Storage interaction such as reads and writes are instead all performed in the top level
//! pallet Call method, which while slightly more verbose, makes it much easier to understand the
Liam Aharon's avatar
Liam Aharon committed
//! code and reason about how storage reads and writes occur in the pallet.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! ## Rewards Algorithm
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! The rewards algorithm is based on the Synthetix [StakingRewards.sol](https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol)
//! smart contract.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! Rewards are calculated JIT (just-in-time), and all operations are O(1) making the approach
//! scalable to many pools and stakers.
Liam Aharon's avatar
Liam Aharon committed
//!
Liam Aharon's avatar
Liam Aharon committed
//! ### Resources
//!
Liam Aharon's avatar
Liam Aharon committed
//! - [This video series](https://www.youtube.com/watch?v=6ZO5aYg1GI8), which walks through the math
//!   of the algorithm.
Liam Aharon's avatar
Liam Aharon committed
//! - [This dev.to article](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f),
//!   which explains the algorithm of the SushiSwap MasterChef staking. While not identical to the
Liam Aharon's avatar
Liam Aharon committed
//!   Synthetix approach, they are quite similar.
Liam Aharon's avatar
Liam Aharon committed
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]

use codec::{Decode, Encode, MaxEncodedLen};
use frame_system::pallet_prelude::BlockNumberFor;
pub use pallet::*;

use frame_support::{
	traits::{
Liam Aharon's avatar
Liam Aharon committed
		fungibles::{Inspect, Mutate},
Liam Aharon's avatar
Liam Aharon committed
		tokens::Balance,
	},
	PalletId,
};
use scale_info::TypeInfo;
use sp_core::Get;
Liam Aharon's avatar
Liam Aharon committed
use sp_runtime::DispatchError;
Liam Aharon's avatar
Liam Aharon committed
use sp_std::boxed::Box;

Liam Aharon's avatar
Liam Aharon committed
#[cfg(feature = "runtime-benchmarks")]
Liam Aharon's avatar
Liam Aharon committed
pub mod benchmarking;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
Liam Aharon's avatar
Liam Aharon committed
mod weights;
Liam Aharon's avatar
Liam Aharon committed
pub use weights::WeightInfo;

/// Unique id type for each pool.
Liam Aharon's avatar
Liam Aharon committed
pub type PoolId = u32;

Liam Aharon's avatar
Liam Aharon committed
/// Multiplier to maintain precision when calculating rewards.
pub(crate) const PRECISION_SCALING_FACTOR: u32 = u32::MAX;

Liam Aharon's avatar
Liam Aharon committed
/// Convenience alias for `PoolInfo`.
Liam Aharon's avatar
Liam Aharon committed
pub type PoolInfoFor<T> = PoolInfo<
	<T as frame_system::Config>::AccountId,
	<T as Config>::AssetId,
	<T as Config>::Balance,
	BlockNumberFor<T>,
>;

Liam Aharon's avatar
Liam Aharon committed
/// The state of a staker in a pool.
#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
Liam Aharon's avatar
Liam Aharon committed
pub struct PoolStakerInfo<Balance> {
Liam Aharon's avatar
Liam Aharon committed
	/// Amount of tokens staked.
Liam Aharon's avatar
Liam Aharon committed
	amount: Balance,
Liam Aharon's avatar
Liam Aharon committed
	/// Accumulated, unpaid rewards.
Liam Aharon's avatar
Liam Aharon committed
	rewards: Balance,
Liam Aharon's avatar
Liam Aharon committed
	/// Reward per token value at the time of the staker's last interaction with the contract.
	reward_per_token_paid: Balance,
Liam Aharon's avatar
Liam Aharon committed
/// The state and configuration an incentive pool.
#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
Liam Aharon's avatar
Liam Aharon committed
pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
Liam Aharon's avatar
Liam Aharon committed
	/// The asset staked in this pool.
Liam Aharon's avatar
Liam Aharon committed
	staked_asset_id: AssetId,
Liam Aharon's avatar
Liam Aharon committed
	/// The asset distributed as rewards by this pool.
Liam Aharon's avatar
Liam Aharon committed
	reward_asset_id: AssetId,
Liam Aharon's avatar
Liam Aharon committed
	/// The amount of tokens rewarded per block.
Liam Aharon's avatar
Liam Aharon committed
	reward_rate_per_block: Balance,
Liam Aharon's avatar
Liam Aharon committed
	/// The block the pool will cease distributing rewards.
	expiry_block: BlockNumber,
	/// The permissioned account that can manage this pool.
	admin: AccountId,
Liam Aharon's avatar
Liam Aharon committed
	/// The total amount of tokens staked in this pool.
Liam Aharon's avatar
Liam Aharon committed
	total_tokens_staked: Balance,
Liam Aharon's avatar
Liam Aharon committed
	/// Total rewards accumulated per token, up to the `last_update_block`.
Liam Aharon's avatar
Liam Aharon committed
	reward_per_token_stored: Balance,
Liam Aharon's avatar
Liam Aharon committed
	/// Last block number the pool was updated.
Liam Aharon's avatar
Liam Aharon committed
	last_update_block: BlockNumber,
Liam Aharon's avatar
Liam Aharon committed
#[frame_support::pallet]
Liam Aharon's avatar
Liam Aharon committed
pub mod pallet {
	use super::*;
Liam Aharon's avatar
Liam Aharon committed
	use frame_support::{
		pallet_prelude::*,
		traits::tokens::{AssetId, Preservation},
	};
Liam Aharon's avatar
Liam Aharon committed
	use frame_system::pallet_prelude::*;
	use sp_runtime::{
		traits::{AccountIdConversion, BadOrigin, EnsureDiv, Saturating},
		DispatchResult,
	};
Liam Aharon's avatar
Liam Aharon committed

	#[pallet::pallet]
	pub struct Pallet<T>(_);

	#[pallet::config]
	pub trait Config: frame_system::Config {
		/// Overarching event type.
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

Liam Aharon's avatar
Liam Aharon committed
		/// The pallet's id, used for deriving pool account IDs.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::constant]
		type PalletId: Get<PalletId>;

		/// Identifier for each type of asset.
Liam Aharon's avatar
Liam Aharon committed
		type AssetId: AssetId + Member + Parameter;
Liam Aharon's avatar
Liam Aharon committed

		/// The type in which the assets are measured.
		type Balance: Balance + TypeInfo;

Liam Aharon's avatar
Liam Aharon committed
		/// The origin with permission to create pools. This will be removed in a later release of
		/// this pallet, which will allow permissionless pool creation.
		///
		/// The Origin must return an AccountId. This can be achieved for any Origin by wrapping it
		/// with `EnsureSuccess`.
		type PermissionedOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
Liam Aharon's avatar
Liam Aharon committed
		/// Registry of assets that can be configured to either stake for rewards, or be offered as
		/// rewards for staking.
		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
Liam Aharon's avatar
Liam Aharon committed
			+ Mutate<Self::AccountId>;
Liam Aharon's avatar
Liam Aharon committed

		/// Weight information for extrinsics in this pallet.
Liam Aharon's avatar
Liam Aharon committed
		type WeightInfo: WeightInfo;
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
		/// Helper for benchmarking.
Liam Aharon's avatar
Liam Aharon committed
		#[cfg(feature = "runtime-benchmarks")]
		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId, Self::AccountId>;
Liam Aharon's avatar
Liam Aharon committed
	/// State of pool stakers.
Liam Aharon's avatar
Liam Aharon committed
	#[pallet::storage]
	pub type PoolStakers<T: Config> = StorageDoubleMap<
		_,
		Blake2_128Concat,
Liam Aharon's avatar
Liam Aharon committed
		PoolId,
Liam Aharon's avatar
Liam Aharon committed
		Blake2_128Concat,
		T::AccountId,
		PoolStakerInfo<T::Balance>,
	>;

	/// State and configuraiton of each staking pool.
	#[pallet::storage]
	pub type Pools<T: Config> = StorageMap<
		_,
		Blake2_128Concat,
Liam Aharon's avatar
Liam Aharon committed
		PoolId,
		PoolInfo<T::AccountId, T::AssetId, T::Balance, BlockNumberFor<T>>,
Liam Aharon's avatar
Liam Aharon committed
	/// Stores the [`PoolId`] to use for the next pool.
	///
	/// Incremented when a new pool is created.
	#[pallet::storage]
	pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
Liam Aharon's avatar
Liam Aharon committed
	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		/// An account staked some tokens in a pool.
Liam Aharon's avatar
Liam Aharon committed
		Staked {
Liam Aharon's avatar
Liam Aharon committed
			/// The account that staked assets.
			who: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
			/// The pool.
Liam Aharon's avatar
Liam Aharon committed
			pool_id: PoolId,
Liam Aharon's avatar
Liam Aharon committed
			/// The staked asset amount.
Liam Aharon's avatar
Liam Aharon committed
			amount: T::Balance,
		},
		/// An account unstaked some tokens from a pool.
Liam Aharon's avatar
Liam Aharon committed
		Unstaked {
Liam Aharon's avatar
Liam Aharon committed
			/// The account that unstaked assets.
			who: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
			/// The pool.
Liam Aharon's avatar
Liam Aharon committed
			pool_id: PoolId,
Liam Aharon's avatar
Liam Aharon committed
			/// The unstaked asset amount.
Liam Aharon's avatar
Liam Aharon committed
			amount: T::Balance,
		},
		/// An account harvested some rewards.
		RewardsHarvested {
Liam Aharon's avatar
Liam Aharon committed
			/// The caller.
			who: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
			/// The staker whos rewards were harvested.
			staker: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
			/// The pool.
Liam Aharon's avatar
Liam Aharon committed
			pool_id: PoolId,
Liam Aharon's avatar
Liam Aharon committed
			/// The amount of harvested tokens.
Liam Aharon's avatar
Liam Aharon committed
			amount: T::Balance,
		},
		/// A new reward pool was created.
		PoolCreated {
			/// The account that created the pool.
			creator: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
			/// The unique ID for the new pool.
Liam Aharon's avatar
Liam Aharon committed
			pool_id: PoolId,
Liam Aharon's avatar
Liam Aharon committed
			/// The staking asset.
Liam Aharon's avatar
Liam Aharon committed
			staked_asset_id: T::AssetId,
Liam Aharon's avatar
Liam Aharon committed
			/// The reward asset.
			reward_asset_id: T::AssetId,
Liam Aharon's avatar
Liam Aharon committed
			/// The initial reward rate per block.
			reward_rate_per_block: T::Balance,
			/// The block the pool will cease to accumulate rewards.
			expiry_block: BlockNumberFor<T>,
			/// The account allowed to modify the pool.
			admin: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
		},
		/// A pool reward rate was modified by the admin.
		PoolRewardRateModified {
Liam Aharon's avatar
Liam Aharon committed
			/// The modified pool.
Liam Aharon's avatar
Liam Aharon committed
			pool_id: PoolId,
			/// The new reward rate per block.
			new_reward_rate_per_block: T::Balance,
		},
Liam Aharon's avatar
Liam Aharon committed
		/// A pool admin was modified.
		PoolAdminModified {
			/// The modified pool.
			pool_id: PoolId,
			/// The new admin.
			new_admin: T::AccountId,
		},
		/// A pool expiry block was modified by the admin.
		PoolExpiryBlockModified {
			/// The modified pool.
			pool_id: PoolId,
			/// The new expiry block.
			new_expiry_block: BlockNumberFor<T>,
Liam Aharon's avatar
Liam Aharon committed
		},
	}

	#[pallet::error]
	pub enum Error<T> {
Liam Aharon's avatar
Liam Aharon committed
		/// The staker does not have enough tokens to perform the operation.
		NotEnoughTokens,
Liam Aharon's avatar
Liam Aharon committed
		/// An operation was attempted on a non-existent pool.
		NonExistentPool,
Liam Aharon's avatar
Liam Aharon committed
		/// An operation was attempted for a non-existent staker.
Liam Aharon's avatar
Liam Aharon committed
		NonExistentStaker,
Liam Aharon's avatar
Liam Aharon committed
		/// An operation was attempted with a non-existent asset.
Liam Aharon's avatar
Liam Aharon committed
		NonExistentAsset,
Liam Aharon's avatar
Liam Aharon committed
		/// There was an error converting a block number.
		BlockNumberConversionError,
Liam Aharon's avatar
Liam Aharon committed
		/// The expiry block must be in the future.
		ExpiryBlockMustBeInTheFuture,
Liam Aharon's avatar
Liam Aharon committed
	}

	/// Pallet's callable functions.
Liam Aharon's avatar
Liam Aharon committed
	///
	/// Allows optionally specifying an admin account for the pool. By default, the origin is made
	/// admin.
Liam Aharon's avatar
Liam Aharon committed
	#[pallet::call(weight(<T as Config>::WeightInfo))]
Liam Aharon's avatar
Liam Aharon committed
	impl<T: Config> Pallet<T> {
		/// Create a new reward pool.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(0)]
Liam Aharon's avatar
Liam Aharon committed
		pub fn create_pool(
			origin: OriginFor<T>,
			staked_asset_id: Box<T::AssetId>,
			reward_asset_id: Box<T::AssetId>,
			reward_rate_per_block: T::Balance,
			expiry_block: BlockNumberFor<T>,
			admin: Option<T::AccountId>,
Liam Aharon's avatar
Liam Aharon committed
		) -> DispatchResult {
			// Check the origin.
			let creator = T::PermissionedOrigin::ensure_origin(origin.clone())?;
Liam Aharon's avatar
Liam Aharon committed
			// Ensure the assets exist.
			ensure!(
				T::Assets::asset_exists(*staked_asset_id.clone()),
				Error::<T>::NonExistentAsset
			);
			ensure!(
				T::Assets::asset_exists(*reward_asset_id.clone()),
				Error::<T>::NonExistentAsset
			);

			// Check the expiry block.
			ensure!(
				expiry_block > frame_system::Pallet::<T>::block_number(),
				Error::<T>::ExpiryBlockMustBeInTheFuture
			);

Liam Aharon's avatar
Liam Aharon committed
			// Get the admin, defaulting to the origin.
			let admin = admin.unwrap_or(creator.clone());
Liam Aharon's avatar
Liam Aharon committed
			let pool = PoolInfoFor::<T> {
Liam Aharon's avatar
Liam Aharon committed
				staked_asset_id: *staked_asset_id.clone(),
				reward_asset_id: *reward_asset_id.clone(),
				reward_rate_per_block,
				total_tokens_staked: 0u32.into(),
Liam Aharon's avatar
Liam Aharon committed
				reward_per_token_stored: 0u32.into(),
				last_update_block: 0u32.into(),
Liam Aharon's avatar
Liam Aharon committed
			// Insert it into storage.
			let pool_id = NextPoolId::<T>::get();
			Pools::<T>::insert(pool_id, pool);
			NextPoolId::<T>::put(pool_id.saturating_add(1));

Liam Aharon's avatar
Liam Aharon committed
			// Emit created event.
			Self::deposit_event(Event::PoolCreated {
				creator,
Liam Aharon's avatar
Liam Aharon committed
				staked_asset_id: *staked_asset_id,
				reward_asset_id: *reward_asset_id,
				reward_rate_per_block,
Liam Aharon's avatar
Liam Aharon committed
		}

		/// Stake tokens in a pool.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(1)]
Liam Aharon's avatar
Liam Aharon committed
		pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
			let caller = ensure_signed(origin)?;

Liam Aharon's avatar
Liam Aharon committed
			// Always start by updating staker and pool rewards.
			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
			let staker_info = PoolStakers::<T>::get(pool_id, &caller).unwrap_or_default();
			let (mut pool_info, mut staker_info) =
				Self::update_pool_and_staker_rewards(pool_info, staker_info)?;
Liam Aharon's avatar
Liam Aharon committed

			// Try to freeze the staker assets.
			// TODO: (blocked https://github.com/paritytech/polkadot-sdk/issues/3342)

			// Update Pools.
Liam Aharon's avatar
Liam Aharon committed
			pool_info.total_tokens_staked.saturating_accrue(amount);
			Pools::<T>::insert(pool_id, pool_info);
Liam Aharon's avatar
Liam Aharon committed

			// Update PoolStakers.
Liam Aharon's avatar
Liam Aharon committed
			staker_info.amount.saturating_accrue(amount);
			PoolStakers::<T>::insert(pool_id, &caller, staker_info);
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			// Emit event.
			Self::deposit_event(Event::Staked { who: caller, pool_id, amount });

Liam Aharon's avatar
Liam Aharon committed
			Ok(())
Liam Aharon's avatar
Liam Aharon committed
		}

		/// Unstake tokens from a pool.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(2)]
Liam Aharon's avatar
Liam Aharon committed
		pub fn unstake(
Liam Aharon's avatar
Liam Aharon committed
			origin: OriginFor<T>,
			pool_id: PoolId,
			amount: T::Balance,
Liam Aharon's avatar
Liam Aharon committed
		) -> DispatchResult {
Liam Aharon's avatar
Liam Aharon committed
			let caller = ensure_signed(origin)?;

			// Always start by updating the pool rewards.
Liam Aharon's avatar
Liam Aharon committed
			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
			let staker_info = PoolStakers::<T>::get(pool_id, &caller).unwrap_or_default();
			let (mut pool_info, mut staker_info) =
				Self::update_pool_and_staker_rewards(pool_info, staker_info)?;
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			// Check the staker has enough staked tokens.
Liam Aharon's avatar
Liam Aharon committed
			ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			// Unfreeze staker assets.
			// TODO: (blocked https://github.com/paritytech/polkadot-sdk/issues/3342)

			// Update Pools.
Liam Aharon's avatar
Liam Aharon committed
			pool_info.total_tokens_staked.saturating_reduce(amount);
			Pools::<T>::insert(pool_id, pool_info);
Liam Aharon's avatar
Liam Aharon committed

			// Update PoolStakers.
Liam Aharon's avatar
Liam Aharon committed
			staker_info.amount.saturating_reduce(amount);
			PoolStakers::<T>::insert(pool_id, &caller, staker_info);
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			// Emit event.
			Self::deposit_event(Event::Unstaked { who: caller, pool_id, amount });

Liam Aharon's avatar
Liam Aharon committed
			Ok(())
Liam Aharon's avatar
Liam Aharon committed
		/// Harvest unclaimed pool rewards for a staker.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(3)]
Liam Aharon's avatar
Liam Aharon committed
		pub fn harvest_rewards(
Liam Aharon's avatar
Liam Aharon committed
			origin: OriginFor<T>,
			pool_id: PoolId,
			staker: Option<T::AccountId>,
Liam Aharon's avatar
Liam Aharon committed
		) -> DispatchResult {
Liam Aharon's avatar
Liam Aharon committed
			let caller = ensure_signed(origin)?;

			let staker = staker.unwrap_or(caller.clone());
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			// Always start by updating the pool and staker rewards.
			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
Liam Aharon's avatar
Liam Aharon committed
			let staker_info =
				PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
Liam Aharon's avatar
Liam Aharon committed
			let (pool_info, mut staker_info) =
				Self::update_pool_and_staker_rewards(pool_info, staker_info)?;
Liam Aharon's avatar
Liam Aharon committed

			// Transfer unclaimed rewards from the pool to the staker.
			let pool_account_id = Self::pool_account_id(&pool_id)?;
			T::Assets::transfer(
				pool_info.reward_asset_id,
				&pool_account_id,
				&staker,
				staker_info.rewards,
				Preservation::Preserve,
			)?;

Liam Aharon's avatar
Liam Aharon committed
			// Emit event.
			Self::deposit_event(Event::RewardsHarvested {
Liam Aharon's avatar
Liam Aharon committed
				who: caller.clone(),
Liam Aharon's avatar
Liam Aharon committed
				staker,
				pool_id,
				amount: staker_info.rewards,
			});

			// Reset staker rewards.
Liam Aharon's avatar
Liam Aharon committed
			staker_info.rewards = 0u32.into();
Liam Aharon's avatar
Liam Aharon committed
			PoolStakers::<T>::insert(pool_id, &caller, staker_info);
Liam Aharon's avatar
Liam Aharon committed

			Ok(())
		/// Modify a pool reward rate.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(4)]
		pub fn set_pool_reward_rate_per_block(
Liam Aharon's avatar
Liam Aharon committed
			origin: OriginFor<T>,
			pool_id: PoolId,
			new_reward_rate_per_block: T::Balance,
Liam Aharon's avatar
Liam Aharon committed
		) -> DispatchResult {
			let caller = T::PermissionedOrigin::ensure_origin(origin.clone())
				.or_else(|_| ensure_signed(origin))?;

			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
			ensure!(caller == pool_info.admin, BadOrigin);
Liam Aharon's avatar
Liam Aharon committed
			// Always start by updating the pool rewards.
			let mut pool_info = Self::update_pool_rewards(pool_info)?;

			pool_info.reward_rate_per_block = new_reward_rate_per_block;
Liam Aharon's avatar
Liam Aharon committed
			Pools::<T>::insert(pool_id, pool_info);

			Self::deposit_event(Event::PoolRewardRateModified {
Liam Aharon's avatar
Liam Aharon committed
				pool_id,
				new_reward_rate_per_block,
			});

			Ok(())
		/// Modify a pool admin.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(5)]
		pub fn set_pool_admin(
			origin: OriginFor<T>,
			pool_id: PoolId,
			new_admin: T::AccountId,
		) -> DispatchResult {
			let caller = T::PermissionedOrigin::ensure_origin(origin.clone())
				.or_else(|_| ensure_signed(origin))?;

			let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
			ensure!(pool_info.admin == caller, BadOrigin);
			pool_info.admin = new_admin.clone();
			Pools::<T>::insert(pool_id, pool_info);

			Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });

			Ok(())
		}

		/// Modify a expiry block.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(6)]
		pub fn set_pool_expiry_block(
			origin: OriginFor<T>,
			pool_id: PoolId,
			new_expiry_block: BlockNumberFor<T>,
		) -> DispatchResult {
			let caller = T::PermissionedOrigin::ensure_origin(origin.clone())
				.or_else(|_| ensure_signed(origin))?;

			ensure!(
				new_expiry_block > frame_system::Pallet::<T>::block_number(),
				Error::<T>::ExpiryBlockMustBeInTheFuture
			);

Liam Aharon's avatar
Liam Aharon committed
			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
Liam Aharon's avatar
Liam Aharon committed
			ensure!(pool_info.admin == caller, BadOrigin);

			// Always start by updating the pool rewards.
Liam Aharon's avatar
Liam Aharon committed
			let mut pool_info = Self::update_pool_rewards(pool_info)?;
			pool_info.expiry_block = new_expiry_block;
			Pools::<T>::insert(pool_id, pool_info);

			Self::deposit_event(Event::PoolExpiryBlockModified { pool_id, new_expiry_block });

			Ok(())
		}

Liam Aharon's avatar
Liam Aharon committed
		/// Convinience method to deposit reward tokens into a pool.
		///
		/// This method is not strictly necessary (tokens could be transferred directly to the
		/// pool pot address), but is provided for convenience so manual derivation of the
		/// account id is not required.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(7)]
Liam Aharon's avatar
Liam Aharon committed
		pub fn deposit_reward_tokens(
Liam Aharon's avatar
Liam Aharon committed
			origin: OriginFor<T>,
			pool_id: PoolId,
			amount: T::Balance,
Liam Aharon's avatar
Liam Aharon committed
		) -> DispatchResult {
Liam Aharon's avatar
Liam Aharon committed
			let caller = ensure_signed(origin)?;
			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
			let pool_account_id = Self::pool_account_id(&pool_id)?;
			T::Assets::transfer(
				pool_info.reward_asset_id,
				&caller,
				&pool_account_id,
				amount,
				Preservation::Preserve,
			)?;
			Ok(())
Liam Aharon's avatar
Liam Aharon committed
		}
Liam Aharon's avatar
Liam Aharon committed

		/// Permissioned method to withdraw reward tokens from a pool.
Liam Aharon's avatar
Liam Aharon committed
		#[pallet::call_index(8)]
Liam Aharon's avatar
Liam Aharon committed
		pub fn withdraw_reward_tokens(
			origin: OriginFor<T>,
			pool_id: PoolId,
			amount: T::Balance,
			dest: T::AccountId,
Liam Aharon's avatar
Liam Aharon committed
		) -> DispatchResult {
			let caller = T::PermissionedOrigin::ensure_origin(origin.clone())
				.or_else(|_| ensure_signed(origin))?;

Liam Aharon's avatar
Liam Aharon committed
			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
			ensure!(pool_info.admin == caller, BadOrigin);
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			T::Assets::transfer(
				pool_info.reward_asset_id,
				&Self::pool_account_id(&pool_id)?,
				&dest,
Liam Aharon's avatar
Liam Aharon committed
				amount,
				Preservation::Preserve,
			)?;

			Ok(())
		}
Liam Aharon's avatar
Liam Aharon committed
	}

	impl<T: Config> Pallet<T> {
Liam Aharon's avatar
Liam Aharon committed
		/// Derive a pool account ID from the pallet's ID.
Liam Aharon's avatar
Liam Aharon committed
		pub fn pool_account_id(id: &PoolId) -> Result<T::AccountId, DispatchError> {
Liam Aharon's avatar
Liam Aharon committed
			if Pools::<T>::contains_key(id) {
				Ok(T::PalletId::get().into_sub_account_truncating(id))
			} else {
				Err(Error::<T>::NonExistentPool.into())
			}
Liam Aharon's avatar
Liam Aharon committed
		/// Computes update pool and staker reward state.
		///
		/// Should be called prior to any operation involving a staker.
		///
		/// Returns the updated pool and staker info.
		///
		/// NOTE: does not modify any storage, that is the responsibility of the caller.
Liam Aharon's avatar
Liam Aharon committed
		pub fn update_pool_and_staker_rewards(
Liam Aharon's avatar
Liam Aharon committed
			pool_info: PoolInfoFor<T>,
Liam Aharon's avatar
Liam Aharon committed
			mut staker_info: PoolStakerInfo<T::Balance>,
		) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
Liam Aharon's avatar
Liam Aharon committed
			let pool_info = Self::update_pool_rewards(pool_info)?;
Liam Aharon's avatar
Liam Aharon committed

			staker_info.rewards = Self::derive_rewards(&pool_info, &staker_info)?;
Liam Aharon's avatar
Liam Aharon committed
			staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
Liam Aharon's avatar
Liam Aharon committed
			return Ok((pool_info, staker_info));
		}

		/// Computes update pool reward state.
		///
		/// Should be called every time the pool is adjusted, and a staker is not involved.
		///
		/// Returns the updated pool and staker info.
		/// NOTE: does not modify any storage, that is the responsibility of the caller.
		pub fn update_pool_rewards(
Liam Aharon's avatar
Liam Aharon committed
			mut pool_info: PoolInfoFor<T>,
		) -> Result<PoolInfoFor<T>, DispatchError> {
			let reward_per_token = Self::reward_per_token(&pool_info)?;
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
			pool_info.reward_per_token_stored = reward_per_token;
Liam Aharon's avatar
Liam Aharon committed
			Ok(pool_info)
Liam Aharon's avatar
Liam Aharon committed
		}

		/// Derives the current reward per token for this pool.
		///
Liam Aharon's avatar
Liam Aharon committed
		/// This is a helper function for `update_pool_rewards` and should not be called directly.
Liam Aharon's avatar
Liam Aharon committed
		fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
Liam Aharon's avatar
Liam Aharon committed
			if pool_info.total_tokens_staked.eq(&0u32.into()) {
Liam Aharon's avatar
Liam Aharon committed
				return Ok(pool_info.reward_per_token_stored)
Liam Aharon's avatar
Liam Aharon committed
			}

			let rewardable_blocks_elapsed: u32 =
				match Self::last_block_reward_applicable(pool_info.expiry_block)
					.saturating_sub(pool_info.last_update_block)
					.try_into()
				{
					Ok(b) => b,
					Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
				};
Liam Aharon's avatar
Liam Aharon committed

Liam Aharon's avatar
Liam Aharon committed
			Ok(pool_info.reward_per_token_stored.saturating_add(
				pool_info
					.reward_rate_per_block
					.saturating_mul(rewardable_blocks_elapsed.into())
Liam Aharon's avatar
Liam Aharon committed
					.saturating_mul(PRECISION_SCALING_FACTOR.into())
					.ensure_div(pool_info.total_tokens_staked)?,
			))
Liam Aharon's avatar
Liam Aharon committed
		}

		/// Derives the amount of rewards earned by a staker.
		///
Liam Aharon's avatar
Liam Aharon committed
		/// This is a helper function for `update_pool_rewards` and should not be called directly.
Liam Aharon's avatar
Liam Aharon committed
		fn derive_rewards(
Liam Aharon's avatar
Liam Aharon committed
			pool_info: &PoolInfoFor<T>,
			staker_info: &PoolStakerInfo<T::Balance>,
Liam Aharon's avatar
Liam Aharon committed
		) -> Result<T::Balance, DispatchError> {
Liam Aharon's avatar
Liam Aharon committed
			let reward_per_token = Self::reward_per_token(&pool_info)?;
Liam Aharon's avatar
Liam Aharon committed

			Ok(staker_info
				.amount
				.saturating_mul(reward_per_token.saturating_sub(staker_info.reward_per_token_paid))
				.ensure_div(PRECISION_SCALING_FACTOR.into())?
				.saturating_add(staker_info.rewards))
Liam Aharon's avatar
Liam Aharon committed
		}

		fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
			if frame_system::Pallet::<T>::block_number() < pool_expiry_block {
				frame_system::Pallet::<T>::block_number()
			} else {
				pool_expiry_block
			}
		}