#![doc = include_str!("../README.md")]
#![allow(clippy::expect_used)]
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(
rustdoc::broken_intra_doc_links,
rustdoc::missing_crate_level_docs,
rustdoc::invalid_codeblock_attributes,
missing_docs
)]
use frame_support::{
ensure,
traits::{
tokens::fungible::{Inspect as InspectFungible, InspectFreeze, Mutate, MutateFreeze},
Get, Hooks,
},
weights::Weight,
};
use sp_runtime::{
traits::{CheckedAdd, Saturating, Zero},
ArithmeticError, DispatchError, Perbill,
};
use sp_std::ops::Mul;
pub use common_primitives::{
capacity::{Nontransferable, Replenishable, TargetValidator},
msa::MessageSourceId,
utils::wrap_binary_data,
};
#[cfg(feature = "runtime-benchmarks")]
use common_primitives::benchmarks::RegisterProviderBenchmarkHelper;
pub use pallet::*;
pub use types::*;
pub use weights::*;
pub mod types;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod tests;
pub mod migration;
pub mod weights;
type BalanceOf<T> =
<<T as Config>::Currency as InspectFungible<<T as frame_system::Config>::AccountId>>::Balance;
use frame_system::pallet_prelude::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
pallet_prelude::{StorageVersion, *},
Twox64Concat,
};
use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay};
#[pallet::composite_enum]
pub enum FreezeReason {
CapacityStaking,
}
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(3);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type RuntimeFreezeReason: From<FreezeReason>;
type WeightInfo: WeightInfo;
type Currency: MutateFreeze<Self::AccountId, Id = Self::RuntimeFreezeReason>
+ Mutate<Self::AccountId>
+ InspectFreeze<Self::AccountId>
+ InspectFungible<Self::AccountId>;
type TargetValidator: TargetValidator;
#[pallet::constant]
type MinimumStakingAmount: Get<BalanceOf<Self>>;
#[pallet::constant]
type MinimumTokenBalance: Get<BalanceOf<Self>>;
#[pallet::constant]
type MaxUnlockingChunks: Get<u32>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: RegisterProviderBenchmarkHelper;
#[pallet::constant]
type UnstakingThawPeriod: Get<u16>;
#[pallet::constant]
type MaxEpochLength: Get<BlockNumberFor<Self>>;
type EpochNumber: Parameter
+ Member
+ MaybeSerializeDeserialize
+ MaybeDisplay
+ AtLeast32BitUnsigned
+ Default
+ Copy
+ sp_std::hash::Hash
+ MaxEncodedLen
+ TypeInfo;
#[pallet::constant]
type CapacityPerToken: Get<Perbill>;
}
#[pallet::storage]
pub type StakingAccountLedger<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, StakingDetails<T>>;
#[pallet::storage]
pub type StakingTargetLedger<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Twox64Concat,
MessageSourceId,
StakingTargetDetails<BalanceOf<T>>,
>;
#[pallet::storage]
pub type CapacityLedger<T: Config> =
StorageMap<_, Twox64Concat, MessageSourceId, CapacityDetails<BalanceOf<T>, T::EpochNumber>>;
#[pallet::storage]
#[pallet::whitelist_storage]
pub type CurrentEpoch<T: Config> = StorageValue<_, T::EpochNumber, ValueQuery>;
#[pallet::storage]
pub type CurrentEpochInfo<T: Config> =
StorageValue<_, EpochInfo<BlockNumberFor<T>>, ValueQuery>;
#[pallet::type_value]
pub fn EpochLengthDefault<T: Config>() -> BlockNumberFor<T> {
100u32.into()
}
#[pallet::storage]
pub type EpochLength<T: Config> =
StorageValue<_, BlockNumberFor<T>, ValueQuery, EpochLengthDefault<T>>;
#[pallet::storage]
pub type UnstakeUnlocks<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, UnlockChunkList<T>>;
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::event]
#[pallet::generate_deposit(pub (super) fn deposit_event)]
pub enum Event<T: Config> {
Staked {
account: T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
capacity: BalanceOf<T>,
},
StakeWithdrawn {
account: T::AccountId,
amount: BalanceOf<T>,
},
UnStaked {
account: T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
capacity: BalanceOf<T>,
},
EpochLengthUpdated {
blocks: BlockNumberFor<T>,
},
CapacityWithdrawn {
msa_id: MessageSourceId,
amount: BalanceOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
InvalidTarget,
InsufficientBalance,
InsufficientStakingAmount,
ZeroAmountNotAllowed,
NotAStakingAccount,
NoUnstakedTokensAvailable,
UnstakedAmountIsZero,
AmountToUnstakeExceedsAmountStaked,
StakerTargetRelationshipNotFound,
TargetCapacityNotFound,
MaxUnlockingChunksExceeded,
IncreaseExceedsAvailable,
MaxEpochLengthExceeded,
BalanceTooLowtoStake,
NoThawedTokenAvailable,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(current: BlockNumberFor<T>) -> Weight {
Self::start_new_epoch_if_needed(current)
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::stake())]
pub fn stake(
origin: OriginFor<T>,
target: MessageSourceId,
amount: BalanceOf<T>,
) -> DispatchResult {
let staker = ensure_signed(origin)?;
let (mut staking_account, actual_amount) =
Self::ensure_can_stake(&staker, target, amount)?;
let capacity = Self::increase_stake_and_issue_capacity(
&staker,
&mut staking_account,
target,
actual_amount,
)?;
Self::deposit_event(Event::Staked {
account: staker,
amount: actual_amount,
target,
capacity,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::withdraw_unstaked())]
pub fn withdraw_unstaked(origin: OriginFor<T>) -> DispatchResult {
let staker = ensure_signed(origin)?;
let amount_withdrawn = Self::do_withdraw_unstaked(&staker)?;
Self::deposit_event(Event::<T>::StakeWithdrawn {
account: staker,
amount: amount_withdrawn,
});
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::unstake())]
pub fn unstake(
origin: OriginFor<T>,
target: MessageSourceId,
requested_amount: BalanceOf<T>,
) -> DispatchResult {
let unstaker = ensure_signed(origin)?;
ensure!(requested_amount > Zero::zero(), Error::<T>::UnstakedAmountIsZero);
let actual_amount = Self::decrease_active_staking_balance(&unstaker, requested_amount)?;
Self::add_unlock_chunk(&unstaker, actual_amount)?;
let capacity_reduction = Self::reduce_capacity(&unstaker, target, actual_amount)?;
Self::deposit_event(Event::UnStaked {
account: unstaker,
target,
amount: actual_amount,
capacity: capacity_reduction,
});
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::set_epoch_length())]
pub fn set_epoch_length(origin: OriginFor<T>, length: BlockNumberFor<T>) -> DispatchResult {
ensure_root(origin)?;
ensure!(length <= T::MaxEpochLength::get(), Error::<T>::MaxEpochLengthExceeded);
EpochLength::<T>::set(length);
Self::deposit_event(Event::EpochLengthUpdated { blocks: length });
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
fn ensure_can_stake(
staker: &T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
) -> Result<(StakingDetails<T>, BalanceOf<T>), DispatchError> {
ensure!(amount > Zero::zero(), Error::<T>::ZeroAmountNotAllowed);
ensure!(T::TargetValidator::validate(target), Error::<T>::InvalidTarget);
let staking_account = StakingAccountLedger::<T>::get(&staker).unwrap_or_default();
let stakable_amount = Self::get_stakable_amount_for(&staker, amount);
ensure!(stakable_amount > Zero::zero(), Error::<T>::BalanceTooLowtoStake);
let new_active_staking_amount = staking_account
.active
.checked_add(&stakable_amount)
.ok_or(ArithmeticError::Overflow)?;
ensure!(
new_active_staking_amount >= T::MinimumStakingAmount::get(),
Error::<T>::InsufficientStakingAmount
);
Ok((staking_account, stakable_amount))
}
fn increase_stake_and_issue_capacity(
staker: &T::AccountId,
staking_account: &mut StakingDetails<T>,
target: MessageSourceId,
amount: BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
staking_account.deposit(amount).ok_or(ArithmeticError::Overflow)?;
let capacity = Self::capacity_generated(amount);
let mut target_details =
StakingTargetLedger::<T>::get(&staker, &target).unwrap_or_default();
target_details.deposit(amount, capacity).ok_or(ArithmeticError::Overflow)?;
let mut capacity_details = CapacityLedger::<T>::get(target).unwrap_or_default();
capacity_details.deposit(&amount, &capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_staking_account_and_lock(&staker, staking_account)?;
Self::set_target_details_for(&staker, target, target_details);
Self::set_capacity_for(target, capacity_details);
Ok(capacity)
}
fn set_staking_account_and_lock(
staker: &T::AccountId,
staking_account: &StakingDetails<T>,
) -> Result<(), DispatchError> {
let unlocks = UnstakeUnlocks::<T>::get(staker).unwrap_or_default();
let total_to_lock: BalanceOf<T> = staking_account
.active
.checked_add(&unlock_chunks_total::<T>(&unlocks))
.ok_or(ArithmeticError::Overflow)?;
T::Currency::set_freeze(&FreezeReason::CapacityStaking.into(), staker, total_to_lock)?;
Self::set_staking_account(staker, staking_account);
Ok(())
}
fn set_staking_account(staker: &T::AccountId, staking_account: &StakingDetails<T>) {
if staking_account.active.is_zero() {
StakingAccountLedger::<T>::set(staker, None);
} else {
StakingAccountLedger::<T>::insert(staker, staking_account);
}
}
fn set_target_details_for(
staker: &T::AccountId,
target: MessageSourceId,
target_details: StakingTargetDetails<BalanceOf<T>>,
) {
if target_details.amount.is_zero() {
StakingTargetLedger::<T>::remove(staker, target);
} else {
StakingTargetLedger::<T>::insert(staker, target, target_details);
}
}
pub fn set_capacity_for(
target: MessageSourceId,
capacity_details: CapacityDetails<BalanceOf<T>, T::EpochNumber>,
) {
CapacityLedger::<T>::insert(target, capacity_details);
}
fn decrease_active_staking_balance(
unstaker: &T::AccountId,
amount: BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
let mut staking_account =
StakingAccountLedger::<T>::get(unstaker).ok_or(Error::<T>::NotAStakingAccount)?;
ensure!(amount <= staking_account.active, Error::<T>::AmountToUnstakeExceedsAmountStaked);
let actual_unstaked_amount = staking_account.withdraw(amount)?;
Self::set_staking_account(unstaker, &staking_account);
Ok(actual_unstaked_amount)
}
fn add_unlock_chunk(
unstaker: &T::AccountId,
actual_unstaked_amount: BalanceOf<T>,
) -> Result<(), DispatchError> {
let current_epoch: T::EpochNumber = CurrentEpoch::<T>::get();
let thaw_at =
current_epoch.saturating_add(T::EpochNumber::from(T::UnstakingThawPeriod::get()));
let mut unlocks = UnstakeUnlocks::<T>::get(unstaker).unwrap_or_default();
let unlock_chunk: UnlockChunk<BalanceOf<T>, T::EpochNumber> =
UnlockChunk { value: actual_unstaked_amount, thaw_at };
unlocks
.try_push(unlock_chunk)
.map_err(|_| Error::<T>::MaxUnlockingChunksExceeded)?;
UnstakeUnlocks::<T>::set(unstaker, Some(unlocks));
Ok(())
}
pub(crate) fn get_stakable_amount_for(
staker: &T::AccountId,
proposed_amount: BalanceOf<T>,
) -> BalanceOf<T> {
let account_balance = T::Currency::balance(&staker);
account_balance
.saturating_sub(T::MinimumTokenBalance::get())
.min(proposed_amount)
}
pub(crate) fn do_withdraw_unstaked(
staker: &T::AccountId,
) -> Result<BalanceOf<T>, DispatchError> {
let current_epoch = CurrentEpoch::<T>::get();
let mut total_unlocking: BalanceOf<T> = Zero::zero();
let mut unlocks =
UnstakeUnlocks::<T>::get(staker).ok_or(Error::<T>::NoUnstakedTokensAvailable)?;
let amount_withdrawn = unlock_chunks_reap_thawed::<T>(&mut unlocks, current_epoch);
ensure!(!amount_withdrawn.is_zero(), Error::<T>::NoThawedTokenAvailable);
if unlocks.is_empty() {
UnstakeUnlocks::<T>::set(staker, None);
} else {
total_unlocking = unlock_chunks_total::<T>(&unlocks);
UnstakeUnlocks::<T>::set(staker, Some(unlocks));
}
let staking_account = StakingAccountLedger::<T>::get(staker).unwrap_or_default();
let total_locked = staking_account.active.saturating_add(total_unlocking);
if total_locked.is_zero() {
T::Currency::thaw(&FreezeReason::CapacityStaking.into(), staker)?;
} else {
T::Currency::set_freeze(&FreezeReason::CapacityStaking.into(), staker, total_locked)?;
}
Ok(amount_withdrawn)
}
fn reduce_capacity(
unstaker: &T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
let mut staking_target_details = StakingTargetLedger::<T>::get(&unstaker, &target)
.ok_or(Error::<T>::StakerTargetRelationshipNotFound)?;
let mut capacity_details =
CapacityLedger::<T>::get(target).ok_or(Error::<T>::TargetCapacityNotFound)?;
let capacity_to_withdraw = if staking_target_details.amount.eq(&amount) {
staking_target_details.capacity
} else {
Self::calculate_capacity_reduction(
amount,
capacity_details.total_tokens_staked,
capacity_details.total_capacity_issued,
)
};
staking_target_details.withdraw(amount, capacity_to_withdraw);
capacity_details.withdraw(capacity_to_withdraw, amount);
Self::set_capacity_for(target, capacity_details);
Self::set_target_details_for(unstaker, target, staking_target_details);
Ok(capacity_to_withdraw)
}
fn capacity_generated(amount: BalanceOf<T>) -> BalanceOf<T> {
let cpt = T::CapacityPerToken::get();
cpt.mul(amount).into()
}
fn calculate_capacity_reduction(
unstaking_amount: BalanceOf<T>,
total_amount_staked: BalanceOf<T>,
total_capacity: BalanceOf<T>,
) -> BalanceOf<T> {
Perbill::from_rational(unstaking_amount, total_amount_staked).mul_ceil(total_capacity)
}
fn start_new_epoch_if_needed(current_block: BlockNumberFor<T>) -> Weight {
if current_block.saturating_sub(CurrentEpochInfo::<T>::get().epoch_start) >=
EpochLength::<T>::get()
{
let current_epoch = CurrentEpoch::<T>::get();
CurrentEpoch::<T>::set(current_epoch.saturating_add(1u32.into()));
CurrentEpochInfo::<T>::set(EpochInfo { epoch_start: current_block });
T::WeightInfo::on_initialize()
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(2))
} else {
T::DbWeight::get().reads(2u64).saturating_add(T::DbWeight::get().writes(1))
}
}
}
impl<T: Config> Nontransferable for Pallet<T> {
type Balance = BalanceOf<T>;
fn balance(msa_id: MessageSourceId) -> Self::Balance {
match CapacityLedger::<T>::get(msa_id) {
Some(capacity_details) => capacity_details.remaining_capacity,
None => BalanceOf::<T>::zero(),
}
}
fn deduct(msa_id: MessageSourceId, amount: Self::Balance) -> Result<(), DispatchError> {
let mut capacity_details =
CapacityLedger::<T>::get(msa_id).ok_or(Error::<T>::TargetCapacityNotFound)?;
capacity_details
.deduct_capacity_by_amount(amount)
.map_err(|_| Error::<T>::InsufficientBalance)?;
Self::set_capacity_for(msa_id, capacity_details);
Self::deposit_event(Event::CapacityWithdrawn { msa_id, amount });
Ok(())
}
fn deposit(msa_id: MessageSourceId, amount: Self::Balance) -> Result<(), DispatchError> {
let mut capacity_details =
CapacityLedger::<T>::get(msa_id).ok_or(Error::<T>::TargetCapacityNotFound)?;
capacity_details.deposit(&amount, &Self::capacity_generated(amount));
Self::set_capacity_for(msa_id, capacity_details);
Ok(())
}
}
impl<T: Config> Replenishable for Pallet<T> {
type Balance = BalanceOf<T>;
fn replenish_all_for(msa_id: MessageSourceId) -> Result<(), DispatchError> {
let mut capacity_details =
CapacityLedger::<T>::get(msa_id).ok_or(Error::<T>::TargetCapacityNotFound)?;
capacity_details.replenish_all(&CurrentEpoch::<T>::get());
Self::set_capacity_for(msa_id, capacity_details);
Ok(())
}
fn replenish_by_amount(
msa_id: MessageSourceId,
amount: Self::Balance,
) -> Result<(), DispatchError> {
let mut capacity_details =
CapacityLedger::<T>::get(msa_id).ok_or(Error::<T>::TargetCapacityNotFound)?;
capacity_details.replenish_by_amount(amount, &CurrentEpoch::<T>::get());
Ok(())
}
fn can_replenish(msa_id: MessageSourceId) -> bool {
if let Some(capacity_details) = CapacityLedger::<T>::get(msa_id) {
return capacity_details.can_replenish(CurrentEpoch::<T>::get());
}
false
}
}