#![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 sp_std::ops::Mul;
use frame_support::{
ensure,
traits::{
fungible::Inspect,
tokens::{
fungible::{Inspect as InspectFungible, InspectFreeze, Mutate, MutateFreeze},
Fortitude, Preservation,
},
Get, Hooks,
},
weights::Weight,
};
use sp_runtime::{
traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero},
ArithmeticError, BoundedVec, DispatchError, Perbill, Permill,
};
pub use common_primitives::{
capacity::*,
msa::MessageSourceId,
node::{AccountId, Balance, BlockNumber},
utils::wrap_binary_data,
};
use frame_system::pallet_prelude::*;
#[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;
type ChunkIndex = u32;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use crate::StakingType::*;
use common_primitives::capacity::RewardEra;
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(4);
#[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::constant]
type EraLength: Get<u32>;
#[pallet::constant]
type ProviderBoostHistoryLimit: Get<u32>;
type RewardsProvider: ProviderBoostRewardsProvider<Self>;
#[pallet::constant]
type MaxRetargetsPerRewardEra: Get<u32>;
#[pallet::constant]
type RewardPoolPerEra: Get<BalanceOf<Self>>;
#[pallet::constant]
type RewardPercentCap: Get<Permill>;
#[pallet::constant]
type RewardPoolChunkLength: Get<u32>;
}
#[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::storage]
pub type Retargets<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RetargetInfo<T>>;
#[pallet::storage]
#[pallet::whitelist_storage]
pub type CurrentEraInfo<T: Config> =
StorageValue<_, RewardEraInfo<RewardEra, BlockNumberFor<T>>, ValueQuery>;
#[pallet::storage]
pub type ProviderBoostRewardPools<T: Config> =
StorageMap<_, Twox64Concat, ChunkIndex, RewardPoolHistoryChunk<T>>;
#[pallet::storage]
pub type CurrentEraProviderBoostTotal<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
pub type ProviderBoostHistories<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, ProviderBoostHistory<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>,
},
StakingTargetChanged {
account: T::AccountId,
from_msa: MessageSourceId,
to_msa: MessageSourceId,
amount: BalanceOf<T>,
},
ProviderBoosted {
account: T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
capacity: BalanceOf<T>,
},
ProviderBoostRewardClaimed {
account: T::AccountId,
reward_amount: BalanceOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
InvalidTarget,
InsufficientCapacityBalance,
StakingAmountBelowMinimum,
ZeroAmountNotAllowed,
NotAStakingAccount,
NoUnstakedTokensAvailable,
UnstakedAmountIsZero,
InsufficientStakingBalance,
StakerTargetRelationshipNotFound,
TargetCapacityNotFound,
MaxUnlockingChunksExceeded,
IncreaseExceedsAvailable,
MaxEpochLengthExceeded,
BalanceTooLowtoStake,
NoThawedTokenAvailable,
CannotChangeStakingType,
EraOutOfRange,
CannotRetargetToSameProvider,
NoRewardsEligibleToClaim,
MustFirstClaimRewards,
MaxRetargetsExceeded,
CollectionBoundExceeded,
NotAProviderBoostAccount,
}
#[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)
.saturating_add(Self::start_new_reward_era_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, MaximumCapacity)?;
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);
ensure!(!Self::has_unclaimed_rewards(&unstaker), Error::<T>::MustFirstClaimRewards);
let (actual_amount, staking_type) =
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, staking_type)?;
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(())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::change_staking_target())]
pub fn change_staking_target(
origin: OriginFor<T>,
from: MessageSourceId,
to: MessageSourceId,
amount: BalanceOf<T>,
) -> DispatchResult {
let staker = ensure_signed(origin)?;
Self::update_retarget_record(&staker)?;
ensure!(from.ne(&to), Error::<T>::CannotRetargetToSameProvider);
ensure!(
amount >= T::MinimumStakingAmount::get(),
Error::<T>::StakingAmountBelowMinimum
);
ensure!(T::TargetValidator::validate(to), Error::<T>::InvalidTarget);
Self::do_retarget(&staker, &from, &to, &amount)?;
Self::deposit_event(Event::StakingTargetChanged {
account: staker,
from_msa: from,
to_msa: to,
amount,
});
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::provider_boost())]
pub fn provider_boost(
origin: OriginFor<T>,
target: MessageSourceId,
amount: BalanceOf<T>,
) -> DispatchResult {
let staker = ensure_signed(origin)?;
let (mut boosting_details, actual_amount) =
Self::ensure_can_boost(&staker, &target, &amount)?;
let capacity = Self::increase_stake_and_issue_boost_capacity(
&staker,
&mut boosting_details,
&target,
&actual_amount,
)?;
Self::deposit_event(Event::ProviderBoosted {
account: staker,
amount: actual_amount,
target,
capacity,
});
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::claim_staking_rewards())]
pub fn claim_staking_rewards(origin: OriginFor<T>) -> DispatchResult {
let staker = ensure_signed(origin)?;
ensure!(
ProviderBoostHistories::<T>::contains_key(staker.clone()),
Error::<T>::NotAProviderBoostAccount
);
let total_to_mint = Self::do_claim_rewards(&staker)?;
Self::deposit_event(Event::ProviderBoostRewardClaimed {
account: staker.clone(),
reward_amount: total_to_mint,
});
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
fn ensure_can_stake(
staker: &T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
staking_type: StakingType,
) -> Result<(StakingDetails<T>, BalanceOf<T>), DispatchError> {
ensure!(amount > Zero::zero(), Error::<T>::ZeroAmountNotAllowed);
ensure!(T::TargetValidator::validate(target), Error::<T>::InvalidTarget);
let staking_details = StakingAccountLedger::<T>::get(&staker).unwrap_or_default();
if !staking_details.active.is_zero() {
ensure!(
staking_details.staking_type.eq(&staking_type),
Error::<T>::CannotChangeStakingType
);
}
let stakable_amount = Self::get_stakable_amount_for(&staker, amount);
ensure!(stakable_amount > Zero::zero(), Error::<T>::BalanceTooLowtoStake);
ensure!(
stakable_amount >= T::MinimumStakingAmount::get(),
Error::<T>::StakingAmountBelowMinimum
);
Ok((staking_details, stakable_amount))
}
fn ensure_can_boost(
staker: &T::AccountId,
target: &MessageSourceId,
amount: &BalanceOf<T>,
) -> Result<(StakingDetails<T>, BalanceOf<T>), DispatchError> {
let (mut staking_details, stakable_amount) =
Self::ensure_can_stake(staker, *target, *amount, StakingType::ProviderBoost)?;
staking_details.staking_type = StakingType::ProviderBoost;
Ok((staking_details, 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 increase_stake_and_issue_boost_capacity(
staker: &T::AccountId,
staking_details: &mut StakingDetails<T>,
target: &MessageSourceId,
amount: &BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
staking_details.deposit(*amount).ok_or(ArithmeticError::Overflow)?;
Self::set_staking_account_and_lock(staker, staking_details)?;
let capacity = Self::capacity_generated(T::RewardsProvider::capacity_boost(*amount));
let mut target_details =
StakingTargetLedger::<T>::get(&staker, &target).unwrap_or_default();
target_details.deposit(*amount, capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_target_details_for(staker, *target, target_details);
let mut capacity_details = CapacityLedger::<T>::get(target).unwrap_or_default();
capacity_details.deposit(amount, &capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_capacity_for(*target, capacity_details);
let era = CurrentEraInfo::<T>::get().era_index;
Self::upsert_boost_history(staker, era, *amount, true)?;
let reward_pool_total = CurrentEraProviderBoostTotal::<T>::get();
CurrentEraProviderBoostTotal::<T>::set(reward_pool_total.saturating_add(*amount));
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>, StakingType), DispatchError> {
let mut staking_account =
StakingAccountLedger::<T>::get(unstaker).ok_or(Error::<T>::NotAStakingAccount)?;
ensure!(amount <= staking_account.active, Error::<T>::InsufficientStakingBalance);
let actual_unstaked_amount = staking_account.withdraw(amount)?;
Self::set_staking_account(unstaker, &staking_account);
let staking_type = staking_account.staking_type;
if staking_type == StakingType::ProviderBoost {
let era = CurrentEraInfo::<T>::get().era_index;
Self::upsert_boost_history(&unstaker, era, actual_unstaked_amount, false)?;
let reward_pool_total = CurrentEraProviderBoostTotal::<T>::get();
CurrentEraProviderBoostTotal::<T>::set(
reward_pool_total.saturating_sub(actual_unstaked_amount),
);
}
Ok((actual_unstaked_amount, staking_type))
}
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::reducible_balance(&staker, Preservation::Preserve, Fortitude::Polite);
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)
}
#[allow(unused)]
fn get_thaw_at_epoch() -> <T as Config>::EpochNumber {
let current_epoch: T::EpochNumber = CurrentEpoch::<T>::get();
let thaw_period = T::UnstakingThawPeriod::get();
current_epoch.saturating_add(thaw_period.into())
}
fn reduce_capacity(
unstaker: &T::AccountId,
target: MessageSourceId,
amount: BalanceOf<T>,
staking_type: StakingType,
) -> Result<BalanceOf<T>, DispatchError> {
let mut staking_target_details = StakingTargetLedger::<T>::get(&unstaker, &target)
.ok_or(Error::<T>::StakerTargetRelationshipNotFound)?;
ensure!(amount.le(&staking_target_details.amount), Error::<T>::InsufficientStakingBalance);
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 {
if staking_type.eq(&StakingType::ProviderBoost) {
Perbill::from_rational(amount, staking_target_details.amount)
.mul_ceil(staking_target_details.capacity)
} else {
Self::calculate_capacity_reduction(
amount,
capacity_details.total_tokens_staked,
capacity_details.total_capacity_issued,
)
}
};
let (actual_amount, actual_capacity) = staking_target_details.withdraw(
amount,
capacity_to_withdraw,
T::MinimumStakingAmount::get(),
);
capacity_details.withdraw(actual_capacity, actual_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::start_new_epoch_if_needed()
} else {
T::DbWeight::get().reads(2u64).saturating_add(T::DbWeight::get().writes(1))
}
}
fn start_new_reward_era_if_needed(current_block: BlockNumberFor<T>) -> Weight {
let current_era_info: RewardEraInfo<RewardEra, BlockNumberFor<T>> =
CurrentEraInfo::<T>::get(); if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() {
let new_era_info = RewardEraInfo {
era_index: current_era_info.era_index.saturating_add(One::one()),
started_at: current_block,
};
CurrentEraInfo::<T>::set(new_era_info); let current_reward_pool_total: BalanceOf<T> = CurrentEraProviderBoostTotal::<T>::get(); Self::update_provider_boost_reward_pool(
current_era_info.era_index,
current_reward_pool_total,
);
T::WeightInfo::start_new_reward_era_if_needed()
} else {
T::DbWeight::get().reads(1)
}
}
fn update_retarget_record(staker: &T::AccountId) -> Result<(), DispatchError> {
let current_era: RewardEra = CurrentEraInfo::<T>::get().era_index;
let mut retargets = Retargets::<T>::get(staker).unwrap_or_default();
ensure!(retargets.update(current_era).is_some(), Error::<T>::MaxRetargetsExceeded);
Retargets::<T>::set(staker, Some(retargets));
Ok(())
}
pub(crate) fn do_retarget(
staker: &T::AccountId,
from_msa: &MessageSourceId,
to_msa: &MessageSourceId,
amount: &BalanceOf<T>,
) -> Result<(), DispatchError> {
let staking_type = StakingAccountLedger::<T>::get(staker).unwrap_or_default().staking_type;
let capacity_withdrawn = Self::reduce_capacity(staker, *from_msa, *amount, staking_type)?;
let mut to_msa_target = StakingTargetLedger::<T>::get(staker, to_msa).unwrap_or_default();
to_msa_target
.deposit(*amount, capacity_withdrawn)
.ok_or(ArithmeticError::Overflow)?;
let mut capacity_details = CapacityLedger::<T>::get(to_msa).unwrap_or_default();
capacity_details
.deposit(amount, &capacity_withdrawn)
.ok_or(ArithmeticError::Overflow)?;
Self::set_target_details_for(staker, *to_msa, to_msa_target);
Self::set_capacity_for(*to_msa, capacity_details);
Ok(())
}
pub(crate) fn upsert_boost_history(
account: &T::AccountId,
current_era: RewardEra,
boost_amount: BalanceOf<T>,
add: bool,
) -> Result<(), DispatchError> {
let mut boost_history = ProviderBoostHistories::<T>::get(account).unwrap_or_default();
let upsert_result = if add {
boost_history.add_era_balance(¤t_era, &boost_amount)
} else {
boost_history.subtract_era_balance(¤t_era, &boost_amount)
};
match upsert_result {
Some(0usize) => ProviderBoostHistories::<T>::remove(account),
None => return Err(DispatchError::from(Error::<T>::EraOutOfRange)),
_ => ProviderBoostHistories::<T>::set(account, Some(boost_history)),
}
Ok(())
}
pub(crate) fn has_unclaimed_rewards(account: &T::AccountId) -> bool {
let current_era = CurrentEraInfo::<T>::get().era_index;
match ProviderBoostHistories::<T>::get(account) {
Some(provider_boost_history) => {
match provider_boost_history.count() {
0usize => false,
1usize => {
provider_boost_history
.get_entry_for_era(¤t_era.saturating_sub(1u32.into()))
.is_none() && provider_boost_history
.get_entry_for_era(¤t_era)
.is_none()
},
_ => true,
}
},
None => false,
} }
pub fn list_unclaimed_rewards(
account: &T::AccountId,
) -> Result<
BoundedVec<
UnclaimedRewardInfo<BalanceOf<T>, BlockNumberFor<T>>,
T::ProviderBoostHistoryLimit,
>,
DispatchError,
> {
if !Self::has_unclaimed_rewards(account) {
return Ok(BoundedVec::new());
}
let staking_history = ProviderBoostHistories::<T>::get(account)
.ok_or(Error::<T>::NotAProviderBoostAccount)?; let current_era_info = CurrentEraInfo::<T>::get(); let max_history: u32 = T::ProviderBoostHistoryLimit::get();
let start_era = current_era_info.era_index.saturating_sub((max_history).into());
let end_era = current_era_info.era_index.saturating_sub(One::one()); let mut previous_amount: BalanceOf<T> = match start_era {
0 => 0u32.into(),
_ =>
staking_history.get_amount_staked_for_era(&(start_era.saturating_sub(1u32.into()))),
};
let mut unclaimed_rewards: BoundedVec<
UnclaimedRewardInfo<BalanceOf<T>, BlockNumberFor<T>>,
T::ProviderBoostHistoryLimit,
> = BoundedVec::new();
for reward_era in start_era..=end_era {
let staked_amount = staking_history.get_amount_staked_for_era(&reward_era);
if !staked_amount.is_zero() {
let expires_at_era = reward_era.saturating_add(max_history.into());
let expires_at_block = Self::block_at_end_of_era(expires_at_era);
let eligible_amount = staked_amount.min(previous_amount);
let total_for_era =
Self::get_total_stake_for_past_era(reward_era, current_era_info.era_index)?;
let earned_amount = <T>::RewardsProvider::era_staking_reward(
eligible_amount,
total_for_era,
T::RewardPoolPerEra::get(),
);
unclaimed_rewards
.try_push(UnclaimedRewardInfo {
reward_era,
expires_at_block,
staked_amount,
eligible_amount,
earned_amount,
})
.map_err(|_e| Error::<T>::CollectionBoundExceeded)?;
previous_amount = staked_amount;
}
} Ok(unclaimed_rewards)
}
pub(crate) fn block_at_end_of_era(era: RewardEra) -> BlockNumberFor<T> {
let current_era_info = CurrentEraInfo::<T>::get();
let era_length: BlockNumberFor<T> = T::EraLength::get().into();
let era_diff = if current_era_info.era_index.eq(&era) {
1u32.into()
} else {
era.saturating_sub(current_era_info.era_index).saturating_add(1u32.into())
};
current_era_info.started_at + era_length.mul(era_diff.into()) - 1u32.into()
}
pub(crate) fn get_total_stake_for_past_era(
reward_era: RewardEra,
current_era: RewardEra,
) -> Result<BalanceOf<T>, DispatchError> {
let era_range = current_era.saturating_sub(reward_era);
ensure!(
current_era.gt(&reward_era) &&
era_range.le(&T::ProviderBoostHistoryLimit::get().into()),
Error::<T>::EraOutOfRange
);
let chunk_idx: ChunkIndex = Self::get_chunk_index_for_era(reward_era);
let reward_pool_chunk = ProviderBoostRewardPools::<T>::get(chunk_idx).unwrap_or_default(); let total_for_era =
reward_pool_chunk.total_for_era(&reward_era).ok_or(Error::<T>::EraOutOfRange)?;
Ok(*total_for_era)
}
pub(crate) fn get_chunk_index_for_era(era: RewardEra) -> u32 {
let history_limit: u32 = T::ProviderBoostHistoryLimit::get();
let chunk_len = T::RewardPoolChunkLength::get();
let era_u32: u32 = era;
let cycle: u32 = era_u32 % history_limit.saturating_add(chunk_len);
cycle.saturating_div(chunk_len)
}
pub(crate) fn update_provider_boost_reward_pool(era: RewardEra, boost_total: BalanceOf<T>) {
let chunk_idx: ChunkIndex = Self::get_chunk_index_for_era(era);
let mut new_chunk = ProviderBoostRewardPools::<T>::get(chunk_idx).unwrap_or_default(); if new_chunk.is_full() {
new_chunk = RewardPoolHistoryChunk::new();
};
if new_chunk.try_insert(era, boost_total).is_err() {
log::warn!("could not insert a new chunk into provider boost reward pool")
}
ProviderBoostRewardPools::<T>::set(chunk_idx, Some(new_chunk)); }
fn do_claim_rewards(staker: &T::AccountId) -> Result<BalanceOf<T>, DispatchError> {
let rewards = Self::list_unclaimed_rewards(&staker)?;
ensure!(!rewards.len().is_zero(), Error::<T>::NoRewardsEligibleToClaim);
let zero_balance: BalanceOf<T> = 0u32.into();
let total_to_mint: BalanceOf<T> = rewards
.iter()
.fold(zero_balance, |acc, reward_info| acc.saturating_add(reward_info.earned_amount))
.into();
ensure!(total_to_mint.gt(&Zero::zero()), Error::<T>::NoRewardsEligibleToClaim);
let _minted_unused = T::Currency::mint_into(&staker, total_to_mint)?;
let mut new_history: ProviderBoostHistory<T> = ProviderBoostHistory::new();
let last_staked_amount =
rewards.last().unwrap_or(&UnclaimedRewardInfo::default()).staked_amount;
let current_era = CurrentEraInfo::<T>::get().era_index;
ensure!(
new_history
.add_era_balance(¤t_era.saturating_sub(1u32.into()), &last_staked_amount)
.is_some(),
Error::<T>::CollectionBoundExceeded
);
ProviderBoostHistories::<T>::set(staker, Some(new_history));
Ok(total_to_mint)
}
}
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>::InsufficientCapacityBalance)?;
Self::set_capacity_for(msa_id, capacity_details);
Self::deposit_event(Event::CapacityWithdrawn { msa_id, amount });
Ok(())
}
fn deposit(
msa_id: MessageSourceId,
token_amount: Self::Balance,
capacity_amount: Self::Balance,
) -> Result<(), DispatchError> {
let mut capacity_details =
CapacityLedger::<T>::get(msa_id).ok_or(Error::<T>::TargetCapacityNotFound)?;
capacity_details.deposit(&token_amount, &capacity_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
}
}
impl<T: Config> ProviderBoostRewardsProvider<T> for Pallet<T> {
type Balance = BalanceOf<T>;
fn reward_pool_size(_total_staked: Self::Balance) -> Self::Balance {
T::RewardPoolPerEra::get()
}
fn era_staking_reward(
era_amount_staked: Self::Balance,
era_total_staked: Self::Balance,
era_reward_pool_size: Self::Balance,
) -> Self::Balance {
let capped_reward = T::RewardPercentCap::get().mul(era_amount_staked);
let proportional_reward = era_reward_pool_size
.saturating_mul(era_amount_staked)
.checked_div(&era_total_staked)
.unwrap_or_else(|| Zero::zero());
proportional_reward.min(capped_reward)
}
fn capacity_boost(amount: Self::Balance) -> Self::Balance {
Perbill::from_percent(STAKED_PERCENTAGE_TO_BOOST).mul(amount)
}
}