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
//!
//! ## Overview
//!
//! Governance can create a new incentive program for a fungible asset by creating a new pool.
//!
//! When creating the pool, governance specifies a 'staking asset', 'reward asset', and 'reward rate
//! per block'.
//!
//! Once the pool is created, holders of the 'staking asset' can stake them in this pallet (creating
//! a new Freeze). Once staked, the staker begins accumulating the right to claim the 'reward asset'
//! each block, proportional to their share of the total staked tokens in the pool.
//!
//! Reward assets pending distribution are held in an account derived from the pallet ID and a
//! unique pool ID.
//!
//! Care should be taken to keep pool accounts adequately funded with the reward asset.
//!
//! ## 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.
//!
//! ## Implementation Notes
//!
//! The implementation is based on the [AccumulatedRewardsPerShare](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f) algorithm.
//!
//! Rewards are calculated JIT (just-in-time), when a staker claims their rewards.
//!
//! All operations are O(1), allowing the approach to scale to an arbitrary amount of pools and
//! stakers.
#![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::{
fungibles::{Balanced, Inspect, Mutate},
tokens::Balance,
},
PalletId,
};
use scale_info::TypeInfo;
use sp_core::Get;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
/// The type of the unique id for each pool.
pub type PoolId = u32;
/// Multiplier to maintain precision when calculating rewards.
pub(crate) const PRECISION_SCALING_FACTOR: u32 = u32::MAX;
#[derive(Debug, Default, 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, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
/// The asset that is staked in this pool.
staking_asset_id: AssetId,
/// The asset that is distributed as rewards in this pool.
reward_asset_id: AssetId,
/// The amount of tokens distributed per block.
/// The total amount of tokens staked in this pool.
/// Total rewards accumulated per token, up to the last time the rewards were updated.
reward_per_token_stored: Balance,
/// Last block number the pool was updated. Used when calculating payouts.
/// The block the pool will cease distributing rewards.
expiry_block: BlockNumber,
/// Permissioned account that can manage this pool.
admin: AccountId,
}
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use frame_support::{
pallet_prelude::*,
traits::tokens::{AssetId, Preservation},
};
use sp_runtime::traits::{AccountIdConversion, BadOrigin, EnsureDiv, Saturating};
#[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>;
/// The pallet's id, used for deriving its sovereign account ID.
#[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.
type PermissionedPoolCreator: EnsureOrigin<Self::RuntimeOrigin>;
/// 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>
+ Mutate<Self::AccountId>
+ Balanced<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 extrinsic caller.
who: T::AccountId,
/// 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 staking asset.
staking_asset_id: T::AssetId,
/// 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 reward pool was deleted by the admin.
/// A pool reward rate was modified by the admin.
PoolRewardRateModified {
/// The new reward rate per block.
new_reward_rate_per_block: T::Balance,
},
/// A pool admin modified by the admin.
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,
/// An operation was attempted using a non-existent asset.
NonExistentAsset,
/// There was an error converting a block number.
BlockNumberConversionError,
/// Expiry block must be in the future.
ExpiryBlockMustBeInTheFuture,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn integrity_test() {
// TODO: Proper implementation
///
/// Allows optionally specifying an admin account for the pool. By default, the origin is made
/// admin.
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Create a new reward pool.
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>,
// Ensure Origin is allowed to create pools.
T::PermissionedPoolCreator::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 origin_acc_id = ensure_signed(origin)?;
let admin = match admin {
Some(admin) => admin,
};
// Create the pool.
let pool = PoolInfo::<T::AccountId, T::AssetId, T::Balance, BlockNumberFor<T>> {
staking_asset_id: *staked_asset_id.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 {
creator: origin_acc_id,
pool_id,
staking_asset_id: *staked_asset_id,
reward_asset_id: *reward_asset_id,
reward_rate_per_block,
admin,
});
Ok(())
}
/// Removes an existing reward pool.
///
/// TODO decide how to manage clean up of stakers from a removed pool.
pub fn remove_pool(_origin: OriginFor<T>, _pool_id: PoolId) -> DispatchResult {
pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
let caller = ensure_signed(origin)?;
// Always start by updating the pool rewards.
Self::update_pool_rewards(&pool_id, Some(&caller))?;
// Try to freeze the staker assets.
// TODO: (blocked https://github.com/paritytech/polkadot-sdk/issues/3342)
// Update Pools.
let mut pool = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
pool.total_tokens_staked.saturating_accrue(amount);
Pools::<T>::insert(pool_id, pool);
// Update PoolStakers.
let mut staker = PoolStakers::<T>::get(pool_id, &caller).unwrap_or_default();
staker.amount.saturating_accrue(amount);
PoolStakers::<T>::insert(pool_id, &caller, staker);
// Emit event.
Self::deposit_event(Event::Staked { who: caller, pool_id, amount });
}
/// Unstake tokens from a pool.
pub fn unstake(
origin: OriginFor<T>,
pool_id: PoolId,
amount: T::Balance,
let caller = ensure_signed(origin)?;
// Always start by updating the pool rewards.
Self::update_pool_rewards(&pool_id, Some(&caller))?;
// Check the staker has enough staked tokens.
let mut staker = PoolStakers::<T>::get(pool_id, &caller).unwrap_or_default();
ensure!(staker.amount >= amount, Error::<T>::NotEnoughTokens);
// Unfreeze staker assets.
// TODO: (blocked https://github.com/paritytech/polkadot-sdk/issues/3342)
// Update Pools.
let mut pool = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
pool.total_tokens_staked.saturating_reduce(amount);
// Update PoolStakers.
staker.amount.saturating_reduce(amount);
// 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 caller = ensure_signed(origin)?;
let staker = match staker {
Some(staker) => staker,
None => caller.clone(),
};
// Always start by updating the pool rewards.
Self::update_pool_rewards(&pool_id, Some(&staker))?;
// Transfer unclaimed rewards from the pool to the staker.
let mut staker_info = PoolStakers::<T>::get(pool_id, &caller).unwrap_or_default();
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,
&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 = ensure_signed(origin)?;
let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
ensure!(pool_info.admin == caller, BadOrigin);
Self::update_pool_rewards(&pool_id, None)?;
pool_info.reward_rate_per_block = new_reward_rate_per_block;
Self::deposit_event(Event::PoolRewardRateModified {
pool_id,
new_reward_rate_per_block,
});
Ok(())
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
/// Modify a pool admin.
pub fn set_pool_admin(
origin: OriginFor<T>,
pool_id: PoolId,
new_admin: T::AccountId,
) -> DispatchResult {
let caller = 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 pool admin.
///
/// TODO: Actually handle this in code
pub fn set_pool_expiry_block(
origin: OriginFor<T>,
pool_id: PoolId,
new_expiry_block: BlockNumberFor<T>,
) -> DispatchResult {
let caller = ensure_signed(origin)?;
ensure!(
new_expiry_block > frame_system::Pallet::<T>::block_number(),
Error::<T>::ExpiryBlockMustBeInTheFuture
);
let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
ensure!(pool_info.admin == caller, BadOrigin);
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.
pub fn deposit_reward_tokens(
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(())
/// 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())
}
/// Update pool reward state, and optionally also a staker's rewards.
pub fn update_pool_rewards(
pool_id: &PoolId,
staker: Option<&T::AccountId>,
) -> DispatchResult {
let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
pool_info.reward_per_token_stored = reward_per_token;
Pools::<T>::insert(pool_id, pool_info);
if let Some(staker) = staker {
let mut staker_info = PoolStakers::<T>::get(pool_id, staker).unwrap_or_default();
staker_info.rewards = Self::derive_rewards(pool_id, staker)?;
staker_info.reward_per_token_paid = reward_per_token;
PoolStakers::<T>::insert(pool_id, staker, staker_info);
}
Ok(())
}
/// Derives the current reward per token for this pool.
///
/// Helper function for update_pool_rewards. Should not be called directly.
fn reward_per_token(pool_id: &PoolId) -> Result<T::Balance, DispatchError> {
let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
if pool_info.total_tokens_staked.eq(&0u32.into()) {
}
let blocks_elapsed: u32 = match frame_system::Pallet::<T>::block_number()
.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(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.
///
/// Helper function for update_pool_rewards. Should not be called directly.
fn derive_rewards(
pool_id: &PoolId,
staker: &T::AccountId,
) -> Result<T::Balance, DispatchError> {
let reward_per_token = Self::reward_per_token(pool_id)?;
let staker_info = PoolStakers::<T>::get(pool_id, staker).unwrap_or_default();
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))