Newer
Older
// 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
//!
//! Allows accounts to be rewarded for holding `fungible` asset/s, for example LP tokens.
//! A permissioned origin can initiate an incentive program for a fungible asset by creating a new
//! pool.
//! 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.
//! Once created, holders of the 'staking asset' can 'stake' them in a corresponding pool, which
//! creates a Freeze on the asset.
//! Once staked, rewards denominated in 'reward asset' begin accumulating to the staker,
//! proportional to their share of the total staked tokens in the pool.
//! Reward assets pending distribution are held in an account unique to each pool.
//! Care should be taken by the pool operator to keep pool accounts adequately funded with the
//! reward asset.
//! The pool admin may adjust the pool configuration such as reward rate per block, expiry block,
//! and admin.
//! ## 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.
//! Note: The permissioned origin must return an AccountId. This can be achieved for any Origin by
//! wrapping it with `EnsureSuccess`.
//!
//! Internal logic functions such as `update_pool_and_staker_rewards` where deliberately written
//!
//! 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
//! code and reason about how storage reads and writes occur in the pallet.
//! The rewards algorithm is based on the Synthetix [StakingRewards.sol](https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol)
//! smart contract.
//! Rewards are calculated JIT (just-in-time), and all operations are O(1) making the approach
//! scalable to many pools and stakers.
//! - [This video series](https://www.youtube.com/watch?v=6ZO5aYg1GI8), which walks through the math
//! of the algorithm.
//! - [This dev.to article](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f),
//! which explains the algorithm of the SushiSwap MasterChef staking. While not identical to the
#![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::{
tokens::Balance,
},
PalletId,
};
use scale_info::TypeInfo;
use sp_core::Get;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub use weights::WeightInfo;
/// Unique id type for each pool.
/// Multiplier to maintain precision when calculating rewards.
pub(crate) const PRECISION_SCALING_FACTOR: u32 = u32::MAX;
pub type PoolInfoFor<T> = PoolInfo<
<T as frame_system::Config>::AccountId,
<T as Config>::AssetId,
<T as Config>::Balance,
BlockNumberFor<T>,
>;
#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
/// Reward per token value at the time of the staker's last interaction with the contract.
reward_per_token_paid: Balance,
#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
/// The block the pool will cease distributing rewards.
expiry_block: BlockNumber,
/// The permissioned account that can manage this pool.
admin: AccountId,
/// The total amount of tokens staked in this pool.
/// Total rewards accumulated per token, up to the `last_update_block`.
use frame_support::{
pallet_prelude::*,
traits::tokens::{AssetId, Preservation},
};
use sp_runtime::{
traits::{AccountIdConversion, BadOrigin, EnsureDiv, Saturating},
DispatchResult,
};
#[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>;
#[pallet::constant]
type PalletId: Get<PalletId>;
/// Identifier for each type of asset.
/// The type in which the assets are measured.
type Balance: Balance + TypeInfo;
/// 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>;
/// 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>
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId, Self::AccountId>;
#[pallet::storage]
pub type PoolStakers<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
Blake2_128Concat,
T::AccountId,
PoolStakerInfo<T::Balance>,
>;
/// State and configuraiton of each staking pool.
#[pallet::storage]
pub type Pools<T: Config> = StorageMap<
_,
Blake2_128Concat,
PoolId,
PoolInfo<T::AccountId, T::AssetId, T::Balance, BlockNumberFor<T>>,
/// 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>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// An account staked some tokens in a pool.
amount: T::Balance,
},
/// An account unstaked some tokens from a pool.
amount: T::Balance,
},
/// An account harvested some rewards.
RewardsHarvested {
/// The staker whos rewards were harvested.
staker: T::AccountId,
amount: T::Balance,
},
/// A new reward pool was created.
PoolCreated {
/// The account that created the pool.
creator: T::AccountId,
/// The reward asset.
reward_asset_id: T::AssetId,
/// 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,
/// A pool reward rate was modified by the admin.
PoolRewardRateModified {
/// The new reward rate per block.
new_reward_rate_per_block: T::Balance,
},
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>,
},
}
#[pallet::error]
pub enum Error<T> {
/// The staker does not have enough tokens to perform the operation.
NotEnoughTokens,
/// An operation was attempted on a non-existent pool.
NonExistentPool,
/// There was an error converting a block number.
BlockNumberConversionError,
ExpiryBlockMustBeInTheFuture,
///
/// Allows optionally specifying an admin account for the pool. By default, the origin is made
/// admin.
impl<T: Config> Pallet<T> {
/// Create a new reward 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>,
// Check the origin.
let creator = T::PermissionedOrigin::ensure_origin(origin.clone())?;
// 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
);
let admin = admin.unwrap_or(creator.clone());
reward_asset_id: *reward_asset_id.clone(),
reward_rate_per_block,
total_tokens_staked: 0u32.into(),
reward_per_token_stored: 0u32.into(),
last_update_block: 0u32.into(),
admin: admin.clone(),
};
let pool_id = NextPoolId::<T>::get();
Pools::<T>::insert(pool_id, pool);
NextPoolId::<T>::put(pool_id.saturating_add(1));
Self::deposit_event(Event::PoolCreated {
reward_asset_id: *reward_asset_id,
reward_rate_per_block,
admin,
});
Ok(())
pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
let caller = ensure_signed(origin)?;
// 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)?;
// Try to freeze the staker assets.
// TODO: (blocked https://github.com/paritytech/polkadot-sdk/issues/3342)
// Update Pools.
pool_info.total_tokens_staked.saturating_accrue(amount);
Pools::<T>::insert(pool_id, pool_info);
staker_info.amount.saturating_accrue(amount);
PoolStakers::<T>::insert(pool_id, &caller, staker_info);
// Emit event.
Self::deposit_event(Event::Staked { who: caller, pool_id, amount });
origin: OriginFor<T>,
pool_id: PoolId,
amount: T::Balance,
let caller = ensure_signed(origin)?;
// Always start by updating the 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)?;
ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
// Unfreeze staker assets.
// TODO: (blocked https://github.com/paritytech/polkadot-sdk/issues/3342)
// Update Pools.
pool_info.total_tokens_staked.saturating_reduce(amount);
Pools::<T>::insert(pool_id, pool_info);
staker_info.amount.saturating_reduce(amount);
PoolStakers::<T>::insert(pool_id, &caller, staker_info);
// Emit event.
Self::deposit_event(Event::Unstaked { who: caller, pool_id, amount });
/// Harvest unclaimed pool rewards for a staker.
origin: OriginFor<T>,
pool_id: PoolId,
staker: Option<T::AccountId>,
let staker = staker.unwrap_or(caller.clone());
// Always start by updating the pool and staker rewards.
let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
let staker_info =
PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
let (pool_info, mut staker_info) =
Self::update_pool_and_staker_rewards(pool_info, staker_info)?;
// 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,
)?;
// Emit event.
Self::deposit_event(Event::RewardsHarvested {
staker,
pool_id,
amount: staker_info.rewards,
});
// Reset staker rewards.
PoolStakers::<T>::insert(pool_id, &caller, staker_info);
/// Modify a pool reward rate.
pub fn set_pool_reward_rate_per_block(
new_reward_rate_per_block: T::Balance,
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);
// 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;
Self::deposit_event(Event::PoolRewardRateModified {
pool_id,
new_reward_rate_per_block,
});
Ok(())
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(())
}
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
);
let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
ensure!(pool_info.admin == caller, BadOrigin);
// Always start by updating the pool rewards.
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(())
}
/// 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.
origin: OriginFor<T>,
pool_id: PoolId,
amount: T::Balance,
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(())
/// Permissioned method to withdraw reward tokens from a pool.
pub fn withdraw_reward_tokens(
origin: OriginFor<T>,
pool_id: PoolId,
amount: T::Balance,
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!(pool_info.admin == caller, BadOrigin);
T::Assets::transfer(
pool_info.reward_asset_id,
&Self::pool_account_id(&pool_id)?,
amount,
Preservation::Preserve,
)?;
Ok(())
}
/// Derive a pool account ID from the pallet's ID.
pub fn pool_account_id(id: &PoolId) -> Result<T::AccountId, DispatchError> {
if Pools::<T>::contains_key(id) {
Ok(T::PalletId::get().into_sub_account_truncating(id))
} else {
Err(Error::<T>::NonExistentPool.into())
}
/// 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.
mut staker_info: PoolStakerInfo<T::Balance>,
) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
let pool_info = Self::update_pool_rewards(pool_info)?;
staker_info.rewards = Self::derive_rewards(&pool_info, &staker_info)?;
staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
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.
mut pool_info: PoolInfoFor<T>,
) -> Result<PoolInfoFor<T>, DispatchError> {
let reward_per_token = Self::reward_per_token(&pool_info)?;
pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
pool_info.reward_per_token_stored = reward_per_token;
}
/// Derives the current reward per token for this pool.
///
/// This is a helper function for `update_pool_rewards` and should not be called directly.
fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
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()),
};
Ok(pool_info.reward_per_token_stored.saturating_add(
pool_info
.reward_rate_per_block
.saturating_mul(rewardable_blocks_elapsed.into())
.saturating_mul(PRECISION_SCALING_FACTOR.into())
.ensure_div(pool_info.total_tokens_staked)?,
))
}
/// Derives the amount of rewards earned by a staker.
///
/// This is a helper function for `update_pool_rewards` and should not be called directly.
pool_info: &PoolInfoFor<T>,
staker_info: &PoolStakerInfo<T::Balance>,
let reward_per_token = Self::reward_per_token(&pool_info)?;
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))
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
}
}