#![cfg_attr(not(feature = "std"), no_std)]
#![recursion_limit = "256"]
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
#[cfg(any(feature = "runtime-benchmarks", test))]
pub mod testing_utils;
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod tests;
pub mod election_size_tracker;
pub mod inflation;
pub mod ledger;
pub mod migrations;
pub mod slashing;
pub mod weights;
mod pallet;
use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use frame_support::{
defensive, defensive_assert,
traits::{
ConstU32, Currency, Defensive, DefensiveMax, DefensiveSaturating, Get, LockIdentifier,
},
weights::Weight,
BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
use scale_info::TypeInfo;
use sp_runtime::{
curve::PiecewiseLinear,
traits::{AtLeast32BitUnsigned, Convert, StaticLookup, Zero},
Perbill, Perquintill, Rounding, RuntimeDebug, Saturating,
};
use sp_staking::{
offence::{Offence, OffenceError, ReportOffence},
EraIndex, ExposurePage, OnStakingUpdate, Page, PagedExposureMetadata, SessionIndex,
StakingAccount,
};
pub use sp_staking::{Exposure, IndividualExposure, StakerStatus};
use sp_std::{collections::btree_map::BTreeMap, prelude::*};
pub use weights::WeightInfo;
pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap};
pub(crate) const STAKING_ID: LockIdentifier = *b"staking ";
pub(crate) const LOG_TARGET: &str = "runtime::staking";
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: crate::LOG_TARGET,
concat!("[{:?}] 💸 ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
)
};
}
pub type MaxWinnersOf<T> = <<T as Config>::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners;
pub type MaxNominationsOf<T> =
<<T as Config>::NominationsQuota as NominationsQuota<BalanceOf<T>>>::MaxNominations;
pub type RewardPoint = u32;
pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::PositiveImbalance;
pub type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::NegativeImbalance;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ActiveEraInfo {
pub index: EraIndex,
pub start: Option<u64>,
}
#[derive(PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct EraRewardPoints<AccountId: Ord> {
pub total: RewardPoint,
pub individual: BTreeMap<AccountId, RewardPoint>,
}
impl<AccountId: Ord> Default for EraRewardPoints<AccountId> {
fn default() -> Self {
EraRewardPoints { total: Default::default(), individual: BTreeMap::new() }
}
}
#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum RewardDestination<AccountId> {
Staked,
Stash,
#[deprecated(
note = "`Controller` will be removed after January 2024. Use `Account(controller)` instead."
)]
Controller,
Account(AccountId),
None,
}
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, Default, MaxEncodedLen)]
pub struct ValidatorPrefs {
#[codec(compact)]
pub commission: Perbill,
pub blocked: bool,
}
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> {
#[codec(compact)]
value: Balance,
#[codec(compact)]
era: EraIndex,
}
#[derive(
PartialEqNoBound,
EqNoBound,
CloneNoBound,
Encode,
Decode,
RuntimeDebugNoBound,
TypeInfo,
MaxEncodedLen,
)]
#[scale_info(skip_type_params(T))]
pub struct StakingLedger<T: Config> {
pub stash: T::AccountId,
#[codec(compact)]
pub total: BalanceOf<T>,
#[codec(compact)]
pub active: BalanceOf<T>,
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
pub legacy_claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>,
#[codec(skip)]
controller: Option<T::AccountId>,
}
#[derive(PartialEq, Debug)]
enum LedgerIntegrityState {
Ok,
Corrupted,
CorruptedKilled,
LockCorrupted,
}
impl<T: Config> StakingLedger<T> {
fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
let mut total = self.total;
let unlocking: BoundedVec<_, _> = self
.unlocking
.into_iter()
.filter(|chunk| {
if chunk.era > current_era {
true
} else {
total = total.saturating_sub(chunk.value);
false
}
})
.collect::<Vec<_>>()
.try_into()
.expect(
"filtering items from a bounded vec always leaves length less than bounds. qed",
);
Self {
stash: self.stash,
total,
active: self.active,
unlocking,
legacy_claimed_rewards: self.legacy_claimed_rewards,
controller: self.controller,
}
}
fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
let mut unlocking_balance = BalanceOf::<T>::zero();
while let Some(last) = self.unlocking.last_mut() {
if unlocking_balance.defensive_saturating_add(last.value) <= value {
unlocking_balance += last.value;
self.active += last.value;
self.unlocking.pop();
} else {
let diff = value.defensive_saturating_sub(unlocking_balance);
unlocking_balance += diff;
self.active += diff;
last.value -= diff;
}
if unlocking_balance >= value {
break
}
}
(self, unlocking_balance)
}
pub fn slash(
&mut self,
slash_amount: BalanceOf<T>,
minimum_balance: BalanceOf<T>,
slash_era: EraIndex,
) -> BalanceOf<T> {
if slash_amount.is_zero() {
return Zero::zero()
}
use sp_runtime::PerThing as _;
let mut remaining_slash = slash_amount;
let pre_slash_total = self.total;
let slashable_chunks_start = slash_era.saturating_add(T::BondingDuration::get());
let (maybe_proportional, slash_chunks_priority) = {
if let Some(first_slashable_index) =
self.unlocking.iter().position(|c| c.era >= slashable_chunks_start)
{
let affected_indices = first_slashable_index..self.unlocking.len();
let unbonding_affected_balance =
affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
if let Some(chunk) = self.unlocking.get(i).defensive() {
sum.saturating_add(chunk.value)
} else {
sum
}
});
let affected_balance = self.active.saturating_add(unbonding_affected_balance);
let ratio = Perquintill::from_rational_with_rounding(
slash_amount,
affected_balance,
Rounding::Up,
)
.unwrap_or_else(|_| Perquintill::one());
(
Some(ratio),
affected_indices.chain((0..first_slashable_index).rev()).collect::<Vec<_>>(),
)
} else {
(None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
}
};
log!(
debug,
"slashing {:?} for era {:?} out of {:?}, priority: {:?}, proportional = {:?}",
slash_amount,
slash_era,
self,
slash_chunks_priority,
maybe_proportional,
);
let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
let mut slash_from_target = if let Some(ratio) = maybe_proportional {
ratio.mul_ceil(*target)
} else {
*slash_remaining
}
.min(*target)
.min(*slash_remaining);
*target = *target - slash_from_target;
if *target < minimum_balance {
slash_from_target =
sp_std::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
}
self.total = self.total.saturating_sub(slash_from_target);
*slash_remaining = slash_remaining.saturating_sub(slash_from_target);
};
slash_out_of(&mut self.active, &mut remaining_slash);
let mut slashed_unlocking = BTreeMap::<_, _>::new();
for i in slash_chunks_priority {
if remaining_slash.is_zero() {
break
}
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
slash_out_of(&mut chunk.value, &mut remaining_slash);
slashed_unlocking.insert(chunk.era, chunk.value);
} else {
break
}
}
self.unlocking.retain(|c| !c.value.is_zero());
let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
T::EventListeners::on_slash(
&self.stash,
self.active,
&slashed_unlocking,
final_slashed_amount,
);
final_slashed_amount
}
}
#[derive(
PartialEqNoBound, EqNoBound, Clone, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,
)]
#[codec(mel_bound())]
#[scale_info(skip_type_params(T))]
pub struct Nominations<T: Config> {
pub targets: BoundedVec<T::AccountId, MaxNominationsOf<T>>,
pub submitted_in: EraIndex,
pub suppressed: bool,
}
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)]
pub struct PagedExposure<AccountId, Balance: HasCompact + codec::MaxEncodedLen> {
exposure_metadata: PagedExposureMetadata<Balance>,
exposure_page: ExposurePage<AccountId, Balance>,
}
impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEncodedLen>
PagedExposure<AccountId, Balance>
{
pub fn from_clipped(exposure: Exposure<AccountId, Balance>) -> Self {
Self {
exposure_metadata: PagedExposureMetadata {
total: exposure.total,
own: exposure.own,
nominator_count: exposure.others.len() as u32,
page_count: 1,
},
exposure_page: ExposurePage { page_total: exposure.total, others: exposure.others },
}
}
pub fn total(&self) -> Balance {
self.exposure_metadata.total
}
pub fn page_total(&self) -> Balance {
self.exposure_page.page_total + self.exposure_metadata.own
}
pub fn own(&self) -> Balance {
self.exposure_metadata.own
}
pub fn others(&self) -> &Vec<IndividualExposure<AccountId, Balance>> {
&self.exposure_page.others
}
}
#[derive(Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct UnappliedSlash<AccountId, Balance: HasCompact> {
validator: AccountId,
own: Balance,
others: Vec<(AccountId, Balance)>,
reporters: Vec<AccountId>,
payout: Balance,
}
impl<AccountId, Balance: HasCompact + Zero> UnappliedSlash<AccountId, Balance> {
pub fn default_from(validator: AccountId) -> Self {
Self {
validator,
own: Zero::zero(),
others: vec![],
reporters: vec![],
payout: Zero::zero(),
}
}
}
pub trait NominationsQuota<Balance> {
type MaxNominations: Get<u32>;
fn get_quota(balance: Balance) -> u32 {
Self::curve(balance).clamp(1, Self::MaxNominations::get())
}
fn curve(balance: Balance) -> u32;
}
pub struct FixedNominationsQuota<const MAX: u32>;
impl<Balance, const MAX: u32> NominationsQuota<Balance> for FixedNominationsQuota<MAX> {
type MaxNominations = ConstU32<MAX>;
fn curve(_: Balance) -> u32 {
MAX
}
}
pub trait SessionInterface<AccountId> {
fn disable_validator(validator_index: u32) -> bool;
fn validators() -> Vec<AccountId>;
fn prune_historical_up_to(up_to: SessionIndex);
}
impl<T: Config> SessionInterface<<T as frame_system::Config>::AccountId> for T
where
T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
T: pallet_session::historical::Config<
FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
FullIdentificationOf = ExposureOf<T>,
>,
T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>,
T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>,
T::ValidatorIdOf: Convert<
<T as frame_system::Config>::AccountId,
Option<<T as frame_system::Config>::AccountId>,
>,
{
fn disable_validator(validator_index: u32) -> bool {
<pallet_session::Pallet<T>>::disable_index(validator_index)
}
fn validators() -> Vec<<T as frame_system::Config>::AccountId> {
<pallet_session::Pallet<T>>::validators()
}
fn prune_historical_up_to(up_to: SessionIndex) {
<pallet_session::historical::Pallet<T>>::prune_up_to(up_to);
}
}
impl<AccountId> SessionInterface<AccountId> for () {
fn disable_validator(_: u32) -> bool {
true
}
fn validators() -> Vec<AccountId> {
Vec::new()
}
fn prune_historical_up_to(_: SessionIndex) {
()
}
}
pub trait EraPayout<Balance> {
fn era_payout(
total_staked: Balance,
total_issuance: Balance,
era_duration_millis: u64,
) -> (Balance, Balance);
}
impl<Balance: Default> EraPayout<Balance> for () {
fn era_payout(
_total_staked: Balance,
_total_issuance: Balance,
_era_duration_millis: u64,
) -> (Balance, Balance) {
(Default::default(), Default::default())
}
}
pub struct ConvertCurve<T>(sp_std::marker::PhantomData<T>);
impl<Balance, T> EraPayout<Balance> for ConvertCurve<T>
where
Balance: AtLeast32BitUnsigned + Clone + Copy,
T: Get<&'static PiecewiseLinear<'static>>,
{
fn era_payout(
total_staked: Balance,
total_issuance: Balance,
era_duration_millis: u64,
) -> (Balance, Balance) {
let (validator_payout, max_payout) = inflation::compute_total_payout(
T::get(),
total_staked,
total_issuance,
era_duration_millis,
);
let rest = max_payout.saturating_sub(validator_payout);
(validator_payout, rest)
}
}
#[derive(
Copy,
Clone,
PartialEq,
Eq,
Encode,
Decode,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
serde::Serialize,
serde::Deserialize,
)]
pub enum Forcing {
NotForcing,
ForceNew,
ForceNone,
ForceAlways,
}
impl Default for Forcing {
fn default() -> Self {
Forcing::NotForcing
}
}
pub struct StashOf<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> Convert<T::AccountId, Option<T::AccountId>> for StashOf<T> {
fn convert(controller: T::AccountId) -> Option<T::AccountId> {
StakingLedger::<T>::paired_account(StakingAccount::Controller(controller))
}
}
pub struct ExposureOf<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> Convert<T::AccountId, Option<Exposure<T::AccountId, BalanceOf<T>>>>
for ExposureOf<T>
{
fn convert(validator: T::AccountId) -> Option<Exposure<T::AccountId, BalanceOf<T>>> {
<Pallet<T>>::active_era()
.map(|active_era| <Pallet<T>>::eras_stakers(active_era.index, &validator))
}
}
pub struct FilterHistoricalOffences<T, R> {
_inner: sp_std::marker::PhantomData<(T, R)>,
}
impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
for FilterHistoricalOffences<Pallet<T>, R>
where
T: Config,
R: ReportOffence<Reporter, Offender, O>,
O: Offence<Offender>,
{
fn report_offence(reporters: Vec<Reporter>, offence: O) -> Result<(), OffenceError> {
let offence_session = offence.session_index();
let bonded_eras = BondedEras::<T>::get();
if bonded_eras.first().filter(|(_, start)| offence_session >= *start).is_some() {
R::report_offence(reporters, offence)
} else {
<Pallet<T>>::deposit_event(Event::<T>::OldSlashingReportDiscarded {
session_index: offence_session,
});
Ok(())
}
}
fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool {
R::is_known_offence(offenders, time_slot)
}
}
pub struct EraInfo<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> EraInfo<T> {
pub fn pending_rewards(era: EraIndex, validator: &T::AccountId) -> bool {
let page_count = if let Some(overview) = <ErasStakersOverview<T>>::get(&era, validator) {
overview.page_count
} else {
if <ErasStakers<T>>::contains_key(era, validator) {
1
} else {
return false
}
};
if <Ledger<T>>::get(validator)
.map(|l| l.legacy_claimed_rewards.contains(&era))
.unwrap_or_default()
{
return false
}
ClaimedRewards::<T>::get(era, validator).len() < page_count as usize
}
pub(crate) fn is_rewards_claimed_with_legacy_fallback(
era: EraIndex,
ledger: &StakingLedger<T>,
validator: &T::AccountId,
page: Page,
) -> bool {
ledger.legacy_claimed_rewards.binary_search(&era).is_ok() ||
Self::is_rewards_claimed(era, validator, page)
}
fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: Page) -> bool {
ClaimedRewards::<T>::get(era, validator).contains(&page)
}
pub fn get_paged_exposure(
era: EraIndex,
validator: &T::AccountId,
page: Page,
) -> Option<PagedExposure<T::AccountId, BalanceOf<T>>> {
let overview = <ErasStakersOverview<T>>::get(&era, validator);
if overview.is_none() && page == 0 {
return Some(PagedExposure::from_clipped(<ErasStakersClipped<T>>::get(era, validator)))
}
if overview.is_none() {
return None
}
let overview = overview.expect("checked above; qed");
let validator_stake = if page == 0 { overview.own } else { Zero::zero() };
let exposure_page = <ErasStakersPaged<T>>::get((era, validator, page)).unwrap_or_default();
Some(PagedExposure {
exposure_metadata: PagedExposureMetadata { own: validator_stake, ..overview },
exposure_page,
})
}
pub fn get_full_exposure(
era: EraIndex,
validator: &T::AccountId,
) -> Exposure<T::AccountId, BalanceOf<T>> {
let overview = <ErasStakersOverview<T>>::get(&era, validator);
if overview.is_none() {
return ErasStakers::<T>::get(era, validator)
}
let overview = overview.expect("checked above; qed");
let mut others = Vec::with_capacity(overview.nominator_count as usize);
for page in 0..overview.page_count {
let nominators = <ErasStakersPaged<T>>::get((era, validator, page));
others.append(&mut nominators.map(|n| n.others).defensive_unwrap_or_default());
}
Exposure { total: overview.total, own: overview.own, others }
}
pub(crate) fn get_page_count(era: EraIndex, validator: &T::AccountId) -> Page {
<ErasStakersOverview<T>>::get(&era, validator)
.map(|overview| {
if overview.page_count == 0 && overview.own > Zero::zero() {
1
} else {
overview.page_count
}
})
.unwrap_or(1)
}
pub(crate) fn get_next_claimable_page(
era: EraIndex,
validator: &T::AccountId,
ledger: &StakingLedger<T>,
) -> Option<Page> {
if Self::is_non_paged_exposure(era, validator) {
return match ledger.legacy_claimed_rewards.binary_search(&era) {
Ok(_) => None,
Err(_) => Some(0),
}
}
let page_count = Self::get_page_count(era, validator);
let all_claimable_pages: Vec<Page> = (0..page_count).collect();
let claimed_pages = ClaimedRewards::<T>::get(era, validator);
all_claimable_pages.into_iter().find(|p| !claimed_pages.contains(p))
}
fn is_non_paged_exposure(era: EraIndex, validator: &T::AccountId) -> bool {
<ErasStakersClipped<T>>::contains_key(&era, validator)
}
pub(crate) fn get_validator_commission(
era: EraIndex,
validator_stash: &T::AccountId,
) -> Perbill {
<ErasValidatorPrefs<T>>::get(&era, validator_stash).commission
}
pub(crate) fn set_rewards_as_claimed(era: EraIndex, validator: &T::AccountId, page: Page) {
let mut claimed_pages = ClaimedRewards::<T>::get(era, validator);
if claimed_pages.contains(&page) {
defensive!("Trying to set an already claimed reward");
return
}
claimed_pages.push(page);
ClaimedRewards::<T>::insert(era, validator, claimed_pages);
}
pub fn set_exposure(
era: EraIndex,
validator: &T::AccountId,
exposure: Exposure<T::AccountId, BalanceOf<T>>,
) {
let page_size = T::MaxExposurePageSize::get().defensive_max(1);
let nominator_count = exposure.others.len();
let expected_page_count = nominator_count
.defensive_saturating_add((page_size as usize).defensive_saturating_sub(1))
.saturating_div(page_size as usize);
let (exposure_metadata, exposure_pages) = exposure.into_pages(page_size);
defensive_assert!(exposure_pages.len() == expected_page_count, "unexpected page count");
<ErasStakersOverview<T>>::insert(era, &validator, &exposure_metadata);
exposure_pages.iter().enumerate().for_each(|(page, paged_exposure)| {
<ErasStakersPaged<T>>::insert((era, &validator, page as Page), &paged_exposure);
});
}
pub(crate) fn set_total_stake(era: EraIndex, total_stake: BalanceOf<T>) {
<ErasTotalStake<T>>::insert(era, total_stake);
}
}
pub trait BenchmarkingConfig {
type MaxValidators: Get<u32>;
type MaxNominators: Get<u32>;
}
#[cfg(feature = "std")]
pub struct TestBenchmarkingConfig;
#[cfg(feature = "std")]
impl BenchmarkingConfig for TestBenchmarkingConfig {
type MaxValidators = frame_support::traits::ConstU32<100>;
type MaxNominators = frame_support::traits::ConstU32<100>;
}
pub trait DisablingStrategy<T: Config> {
fn decision(
offender_stash: &T::AccountId,
slash_era: EraIndex,
currently_disabled: &Vec<u32>,
) -> Option<u32>;
}
pub struct UpToLimitDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;
impl<const DISABLING_LIMIT_FACTOR: usize> UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR> {
pub fn disable_limit(validators_len: usize) -> usize {
validators_len
.saturating_sub(1)
.checked_div(DISABLING_LIMIT_FACTOR)
.unwrap_or_else(|| {
defensive!("DISABLING_LIMIT_FACTOR should not be 0");
0
})
}
}
impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
for UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>
{
fn decision(
offender_stash: &T::AccountId,
slash_era: EraIndex,
currently_disabled: &Vec<u32>,
) -> Option<u32> {
let active_set = T::SessionInterface::validators();
if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
log!(
debug,
"Won't disable: reached disabling limit {:?}",
Self::disable_limit(active_set.len())
);
return None
}
if ActiveEra::<T>::get().map(|e| e.index).unwrap_or_default() > slash_era {
log!(
debug,
"Won't disable: current_era {:?} > slash_era {:?}",
Pallet::<T>::current_era().unwrap_or_default(),
slash_era
);
return None
}
let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) {
idx as u32
} else {
log!(debug, "Won't disable: offender not in active set",);
return None
};
log!(debug, "Will disable {:?}", offender_idx);
Some(offender_idx)
}
}