// This file is part of Bizinikiwi. // 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. //! `pezpallet-staking-async`'s main `pezpallet` module. use crate::{ asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, EraRewardPoints, ExposurePage, Forcing, LedgerIntegrityState, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, }; use alloc::{format, vec::Vec}; use codec::Codec; pub use impls::*; use pezframe_election_provider_support::{ElectionProvider, SortedListProvider, VoteWeight}; use pezframe_support::{ assert_ok, pezpallet_prelude::*, traits::{ fungible::{ hold::{Balanced as FunHoldBalanced, Mutate as FunHoldMutate}, Mutate, Mutate as FunMutate, }, Contains, Defensive, DefensiveSaturating, EnsureOrigin, Get, InspectLockableCurrency, Nothing, OnUnbalanced, }, weights::Weight, BoundedBTreeSet, BoundedVec, }; use pezframe_system::{ensure_root, ensure_signed, pezpallet_prelude::*}; use pezsp_core::{sr25519::Pair as SrPair, Pair}; use pezsp_runtime::{ traits::{StaticLookup, Zero}, ArithmeticError, Perbill, Percent, }; use pezsp_staking::{ EraIndex, Page, SessionIndex, StakingAccount::{self, Controller, Stash}, StakingInterface, }; use rand::seq::SliceRandom; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaChaRng, }; mod impls; #[pezframe_support::pezpallet] pub mod pezpallet { use core::ops::Deref; use super::*; use crate::{session_rotation, PagedExposureMetadata, SnapshotStatus}; use codec::HasCompact; use pezframe_election_provider_support::{ElectionDataProvider, PageIndex}; use pezframe_support::DefaultNoBound; /// Represents the current step in the era pruning process #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum PruningStep { /// Pruning ErasStakersPaged storage ErasStakersPaged, /// Pruning ErasStakersOverview storage ErasStakersOverview, /// Pruning ErasValidatorPrefs storage ErasValidatorPrefs, /// Pruning ClaimedRewards storage ClaimedRewards, /// Pruning ErasValidatorReward storage ErasValidatorReward, /// Pruning ErasRewardPoints storage ErasRewardPoints, /// Pruning ErasTotalStake storage ErasTotalStake, } /// The in-code storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(17); #[pezpallet::pezpallet] #[pezpallet::storage_version(STORAGE_VERSION)] pub struct Pezpallet(_); /// Possible operations on the configuration values of this pezpallet. #[derive(TypeInfo, Debug, Clone, Encode, Decode, DecodeWithMemTracking, PartialEq)] pub enum ConfigOp { /// Don't change. Noop, /// Set the given value. Set(T), /// Remove from storage. Remove, } #[pezpallet::config(with_default)] pub trait Config: pezframe_system::Config { /// The old trait for staking balance. Deprecated and only used for migrating old ledgers. #[pezpallet::no_default] type OldCurrency: InspectLockableCurrency< Self::AccountId, Moment = BlockNumberFor, Balance = Self::CurrencyBalance, >; /// The staking balance. #[pezpallet::no_default] type Currency: FunHoldMutate< Self::AccountId, Reason = Self::RuntimeHoldReason, Balance = Self::CurrencyBalance, > + FunMutate + FunHoldBalanced; /// Overarching hold reason. #[pezpallet::no_default_bounds] type RuntimeHoldReason: From; /// Just the `Currency::Balance` type; we have this item to allow us to constrain it to /// `From`. type CurrencyBalance: pezsp_runtime::traits::AtLeast32BitUnsigned + codec::FullCodec + DecodeWithMemTracking + HasCompact + Copy + MaybeSerializeDeserialize + core::fmt::Debug + Default + From + TypeInfo + Send + Sync + MaxEncodedLen; /// Convert a balance into a number used for election calculation. This must fit into a /// `u64` but is allowed to be sensibly lossy. The `u64` is used to communicate with the /// [`pezframe_election_provider_support`] crate which accepts u64 numbers and does /// operations in 128. /// Consequently, the backward convert is used convert the u128s from sp-elections back to a /// [`BalanceOf`]. #[pezpallet::no_default_bounds] type CurrencyToVote: pezsp_staking::currency_to_vote::CurrencyToVote>; /// Something that provides the election functionality. #[pezpallet::no_default] type ElectionProvider: ElectionProvider< AccountId = Self::AccountId, BlockNumber = BlockNumberFor, // we only accept an election provider that has staking as data provider. DataProvider = Pezpallet, >; /// Something that defines the maximum number of nominations per nominator. #[pezpallet::no_default_bounds] type NominationsQuota: NominationsQuota>; /// Number of eras to keep in history. /// /// Following information is kept for eras in `[current_era - /// HistoryDepth, current_era]`: `ErasValidatorPrefs`, `ErasValidatorReward`, /// `ErasRewardPoints`, `ErasTotalStake`, `ClaimedRewards`, /// `ErasStakersPaged`, `ErasStakersOverview`. /// /// Must be more than the number of eras delayed by session. /// I.e. active era must always be in history. I.e. `active_era > /// current_era - history_depth` must be guaranteed. /// /// If migrating an existing pezpallet from storage value to config value, /// this should be set to same value or greater as in storage. #[pezpallet::constant] type HistoryDepth: Get; /// Tokens have been minted and are unused for validator-reward. /// See [Era payout](./index.html#era-payout). #[pezpallet::no_default_bounds] type RewardRemainder: OnUnbalanced>; /// Handler for the unbalanced reduction when slashing a staker. #[pezpallet::no_default_bounds] type Slash: OnUnbalanced>; /// Handler for the unbalanced increment when rewarding a staker. /// NOTE: in most cases, the implementation of `OnUnbalanced` should modify the total /// issuance. #[pezpallet::no_default_bounds] type Reward: OnUnbalanced>; /// Number of sessions per era, as per the preferences of the **relay chain**. #[pezpallet::constant] type SessionsPerEra: Get; /// Number of sessions before the end of an era when the election for the next era will /// start. /// /// - This determines how many sessions **before** the last session of the era the staking /// election process should begin. /// - The value is bounded between **1** (election starts at the beginning of the last /// session) and `SessionsPerEra` (election starts at the beginning of the first session /// of the era). /// /// ### Example: /// - If `SessionsPerEra = 6` and `PlanningEraOffset = 1`, the election starts at the /// beginning of session `6 - 1 = 5`. /// - If `PlanningEraOffset = 6`, the election starts at the beginning of session `6 - 6 = /// 0`, meaning it starts at the very beginning of the era. #[pezpallet::constant] type PlanningEraOffset: Get; /// Number of eras that staked funds must remain bonded for. #[pezpallet::constant] type BondingDuration: Get; /// Number of eras that slashes are deferred by, after computation. /// /// This should be less than the bonding duration. Set to 0 if slashes /// should be applied immediately, without opportunity for intervention. #[pezpallet::constant] type SlashDeferDuration: Get; /// The origin which can manage less critical staking parameters that does not require root. /// /// Supported actions: (1) cancel deferred slash, (2) set minimum commission. #[pezpallet::no_default] type AdminOrigin: EnsureOrigin; /// The payout for validators and the system for the current era. /// See [Era payout](./index.html#era-payout). #[pezpallet::no_default] type EraPayout: EraPayout>; /// The maximum size of each `T::ExposurePage`. /// /// An `ExposurePage` is weakly bounded to a maximum of `MaxExposurePageSize` /// nominators. /// /// For older non-paged exposure, a reward payout was restricted to the top /// `MaxExposurePageSize` nominators. This is to limit the i/o cost for the /// nominator payout. /// /// Note: `MaxExposurePageSize` is used to bound `ClaimedRewards` and is unsafe to /// reduce without handling it in a migration. #[pezpallet::constant] type MaxExposurePageSize: Get; /// The absolute maximum of winner validators this pezpallet should return. /// /// As this pezpallet supports multi-block election, the set of winner validators *per /// election* is bounded by this type. #[pezpallet::constant] type MaxValidatorSet: Get; /// Something that provides a best-effort sorted list of voters aka electing nominators, /// used for NPoS election. /// /// The changes to nominators are reported to this. Moreover, each validator's self-vote is /// also reported as one independent vote. /// /// To keep the load off the chain as much as possible, changes made to the staked amount /// via rewards and slashes are not reported and thus need to be manually fixed by the /// staker. In case of `bags-list`, this always means using `rebag` and `putInFrontOf`. /// /// Invariant: what comes out of this list will always be a nominator. #[pezpallet::no_default] type VoterList: SortedListProvider; /// WIP: This is a noop as of now, the actual business logic that's described below is going /// to be introduced in a follow-up PR. /// /// Something that provides a best-effort sorted list of targets aka electable validators, /// used for NPoS election. /// /// The changes to the approval stake of each validator are reported to this. This means any /// change to: /// 1. The stake of any validator or nominator. /// 2. The targets of any nominator /// 3. The role of any staker (e.g. validator -> chilled, nominator -> validator, etc) /// /// Unlike `VoterList`, the values in this list are always kept up to date with reward and /// slash as well, and thus represent the accurate approval stake of all account being /// nominated by nominators. /// /// Note that while at the time of nomination, all targets are checked to be real /// validators, they can chill at any point, and their approval stakes will still be /// recorded. This implies that what comes out of iterating this list MIGHT NOT BE AN ACTIVE /// VALIDATOR. #[pezpallet::no_default] type TargetList: SortedListProvider>; /// The maximum number of `unlocking` chunks a [`StakingLedger`] can /// have. Effectively determines how many unique eras a staker may be /// unbonding in. /// /// Note: `MaxUnlockingChunks` is used as the upper bound for the /// `BoundedVec` item `StakingLedger.unlocking`. Setting this value /// lower than the existing value can lead to inconsistencies in the /// `StakingLedger` and will need to be handled properly in a runtime /// migration. The test `reducing_max_unlocking_chunks_abrupt` shows /// this effect. #[pezpallet::constant] type MaxUnlockingChunks: Get; /// The maximum amount of controller accounts that can be deprecated in one call. type MaxControllersInDeprecationBatch: Get; /// Something that listens to staking updates and performs actions based on the data it /// receives. /// /// WARNING: this only reports slashing and withdraw events for the time being. #[pezpallet::no_default_bounds] type EventListeners: pezsp_staking::OnStakingUpdate>; /// Maximum number of invulnerable validators. #[pezpallet::constant] type MaxInvulnerables: Get; /// Maximum allowed era duration in milliseconds. /// /// This provides a defensive upper bound to cap the effective era duration, preventing /// excessively long eras from causing runaway inflation (e.g., due to bugs). If the actual /// era duration exceeds this value, it will be clamped to this maximum. /// /// Example: For an ideal era duration of 24 hours (86,400,000 ms), /// this can be set to 604,800,000 ms (7 days). #[pezpallet::constant] type MaxEraDuration: Get; /// Maximum number of storage items that can be pruned in a single call. /// /// This controls how many storage items can be deleted in each call to `prune_era_step`. /// This should be set to a conservative value (e.g., 100-500 items) to ensure pruning /// doesn't consume too much block space. The actual weight is determined by benchmarks. #[pezpallet::constant] type MaxPruningItems: Get; /// Interface to talk to the RC-Client pezpallet, possibly sending election results to the /// relay chain. #[pezpallet::no_default] type RcClientInterface: pezpallet_staking_async_rc_client::RcClientInterface< AccountId = Self::AccountId, >; #[pezpallet::no_default_bounds] /// Filter some accounts from participating in staking. /// /// This is useful for example to blacklist an account that is participating in staking in /// another way (such as pools). type Filter: Contains; /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; } /// A reason for placing a hold on funds. #[pezpallet::composite_enum] pub enum HoldReason { /// Funds on stake by a nominator or a validator. #[codec(index = 0)] Staking, } /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. pub mod config_preludes { use super::*; use pezframe_support::{derive_impl, parameter_types, traits::ConstU32}; pub struct TestDefaultConfig; #[derive_impl(pezframe_system::config_preludes::TestDefaultConfig, no_aggregated_types)] impl pezframe_system::DefaultConfig for TestDefaultConfig {} parameter_types! { pub const SessionsPerEra: SessionIndex = 3; pub const BondingDuration: EraIndex = 3; pub const MaxPruningItems: u32 = 100; } #[pezframe_support::register_default_impl(TestDefaultConfig)] impl DefaultConfig for TestDefaultConfig { #[inject_runtime_type] type RuntimeHoldReason = (); type CurrencyBalance = u128; type CurrencyToVote = (); type NominationsQuota = crate::FixedNominationsQuota<16>; type HistoryDepth = ConstU32<84>; type RewardRemainder = (); type Slash = (); type Reward = (); type SessionsPerEra = SessionsPerEra; type BondingDuration = BondingDuration; type PlanningEraOffset = ConstU32<1>; type SlashDeferDuration = (); type MaxExposurePageSize = ConstU32<64>; type MaxUnlockingChunks = ConstU32<32>; type MaxValidatorSet = ConstU32<100>; type MaxControllersInDeprecationBatch = ConstU32<100>; type MaxInvulnerables = ConstU32<20>; type MaxEraDuration = (); type MaxPruningItems = MaxPruningItems; type EventListeners = (); type Filter = Nothing; type WeightInfo = (); } } /// The ideal number of active validators. #[pezpallet::storage] pub type ValidatorCount = StorageValue<_, u32, ValueQuery>; /// Any validators that may never be slashed or forcibly kicked. It's a Vec since they're /// easy to initialize and the performance hit is minimal (we expect no more than four /// invulnerables) and restricted to testnets. #[pezpallet::storage] pub type Invulnerables = StorageValue<_, BoundedVec, ValueQuery>; /// Map from all locked "stash" accounts to the controller account. /// /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. #[pezpallet::storage] pub type Bonded = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>; /// The minimum active bond to become and maintain the role of a nominator. #[pezpallet::storage] pub type MinNominatorBond = StorageValue<_, BalanceOf, ValueQuery>; /// The minimum active bond to become and maintain the role of a validator. #[pezpallet::storage] pub type MinValidatorBond = StorageValue<_, BalanceOf, ValueQuery>; /// The minimum active nominator stake of the last successful election. #[pezpallet::storage] pub type MinimumActiveStake = StorageValue<_, BalanceOf, ValueQuery>; /// The minimum amount of commission that validators can set. /// /// If set to `0`, no limit exists. #[pezpallet::storage] pub type MinCommission = StorageValue<_, Perbill, ValueQuery>; /// Map from all (unlocked) "controller" accounts to the info regarding the staking. /// /// Note: All the reads and mutations to this storage *MUST* be done through the methods exposed /// by [`StakingLedger`] to ensure data and lock consistency. #[pezpallet::storage] pub type Ledger = StorageMap<_, Blake2_128Concat, T::AccountId, StakingLedger>; /// Where the reward payment should be made. Keyed by stash. /// /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. #[pezpallet::storage] pub type Payee = StorageMap<_, Twox64Concat, T::AccountId, RewardDestination, OptionQuery>; /// The map from (wannabe) validator stash key to the preferences of that validator. /// /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. #[pezpallet::storage] pub type Validators = CountedStorageMap<_, Twox64Concat, T::AccountId, ValidatorPrefs, ValueQuery>; /// The maximum validator count before we stop allowing new validators to join. /// /// When this value is not set, no limits are enforced. #[pezpallet::storage] pub type MaxValidatorsCount = StorageValue<_, u32, OptionQuery>; /// The map from nominator stash key to their nomination preferences, namely the validators that /// they wish to support. /// /// Note that the keys of this storage map might become non-decodable in case the /// account's [`NominationsQuota::MaxNominations`] configuration is decreased. /// In this rare case, these nominators /// are still existent in storage, their key is correct and retrievable (i.e. `contains_key` /// indicates that they exist), but their value cannot be decoded. Therefore, the non-decodable /// nominators will effectively not-exist, until they re-submit their preferences such that it /// is within the bounds of the newly set `Config::MaxNominations`. /// /// This implies that `::iter_keys().count()` and `::iter().count()` might return different /// values for this map. Moreover, the main `::count()` is aligned with the former, namely the /// number of keys that exist. /// /// Lastly, if any of the nominators become non-decodable, they can be chilled immediately via /// [`Call::chill_other`] dispatchable by anyone. /// /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. #[pezpallet::storage] pub type Nominators = CountedStorageMap<_, Twox64Concat, T::AccountId, Nominations>; /// Stakers whose funds are managed by other pallets. /// /// This pezpallet does not apply any locks on them, therefore they are only virtually bonded. /// They are expected to be keyless accounts and hence should not be allowed to mutate their /// ledger directly via this pezpallet. Instead, these accounts are managed by other pallets /// and accessed via low level apis. We keep track of them to do minimal integrity checks. #[pezpallet::storage] pub type VirtualStakers = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; /// The maximum nominator count before we stop allowing new validators to join. /// /// When this value is not set, no limits are enforced. #[pezpallet::storage] pub type MaxNominatorsCount = StorageValue<_, u32, OptionQuery>; // --- AUDIT NOTE: the following storage items should only be controlled by `Rotator` /// The current planned era index. /// /// This is the latest planned era, depending on how the Session pezpallet queues the validator /// set, it might be active or not. #[pezpallet::storage] pub type CurrentEra = StorageValue<_, EraIndex>; /// The active era information, it holds index and start. /// /// The active era is the era being currently rewarded. Validator set of this era must be /// equal to what is RC's session pezpallet. #[pezpallet::storage] pub type ActiveEra = StorageValue<_, ActiveEraInfo>; /// Custom bound for [`BondedEras`] which is equal to [`Config::BondingDuration`] + 1. pub struct BondedErasBound(core::marker::PhantomData); impl Get for BondedErasBound { fn get() -> u32 { T::BondingDuration::get().saturating_add(1) } } /// A mapping from still-bonded eras to the first session index of that era. /// /// Must contains information for eras for the range: /// `[active_era - bounding_duration; active_era]` #[pezpallet::storage] pub type BondedEras = StorageValue<_, BoundedVec<(EraIndex, SessionIndex), BondedErasBound>, ValueQuery>; // --- AUDIT Note: end of storage items controlled by `Rotator`. /// Summary of validator exposure at a given era. /// /// This contains the total stake in support of the validator and their own stake. In addition, /// it can also be used to get the number of nominators backing this validator and the number of /// exposure pages they are divided into. The page count is useful to determine the number of /// pages of rewards that needs to be claimed. /// /// This is keyed first by the era index to allow bulk deletion and then the stash account. /// Should only be accessed through `Eras`. /// /// Is it removed after [`Config::HistoryDepth`] eras. /// If stakers hasn't been set or has been removed then empty overview is returned. #[pezpallet::storage] pub type ErasStakersOverview = StorageDoubleMap< _, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, PagedExposureMetadata>, OptionQuery, >; /// A bounded wrapper for [`pezsp_staking::ExposurePage`]. /// /// It has `Deref` and `DerefMut` impls that map it back [`pezsp_staking::ExposurePage`] for all /// purposes. This is done in such a way because we prefer to keep the types in /// [`pezsp_staking`] pure, and not polluted by pezpallet-specific bounding logic. /// /// It encoded and decodes exactly the same as [`pezsp_staking::ExposurePage`], and provides a /// manual `MaxEncodedLen` implementation, to be used in benchmarking #[derive(PartialEqNoBound, Encode, Decode, DebugNoBound, TypeInfo, DefaultNoBound)] #[scale_info(skip_type_params(T))] pub struct BoundedExposurePage(pub ExposurePage>); impl Deref for BoundedExposurePage { type Target = ExposurePage>; fn deref(&self) -> &Self::Target { &self.0 } } impl core::ops::DerefMut for BoundedExposurePage { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl codec::MaxEncodedLen for BoundedExposurePage { fn max_encoded_len() -> usize { let max_exposure_page_size = T::MaxExposurePageSize::get() as usize; let individual_size = T::AccountId::max_encoded_len() + BalanceOf::::max_encoded_len(); // 1 balance for `total` BalanceOf::::max_encoded_len() + // individual_size multiplied by page size max_exposure_page_size.saturating_mul(individual_size) } } impl From>> for BoundedExposurePage { fn from(value: ExposurePage>) -> Self { Self(value) } } impl From> for ExposurePage> { fn from(value: BoundedExposurePage) -> Self { value.0 } } impl codec::EncodeLike> for ExposurePage> { } /// Paginated exposure of a validator at given era. /// /// This is keyed first by the era index to allow bulk deletion, then stash account and finally /// the page. Should only be accessed through `Eras`. /// /// This is cleared after [`Config::HistoryDepth`] eras. #[pezpallet::storage] pub type ErasStakersPaged = StorageNMap< _, ( NMapKey, NMapKey, NMapKey, ), BoundedExposurePage, OptionQuery, >; pub struct ClaimedRewardsBound(core::marker::PhantomData); impl Get for ClaimedRewardsBound { fn get() -> u32 { let max_total_nominators_per_validator = ::MaxBackersPerWinnerFinal::get(); let exposure_page_size = T::MaxExposurePageSize::get(); max_total_nominators_per_validator .saturating_div(exposure_page_size) .saturating_add(1) } } /// History of claimed paged rewards by era and validator. /// /// This is keyed by era and validator stash which maps to the set of page indexes which have /// been claimed. /// /// It is removed after [`Config::HistoryDepth`] eras. #[pezpallet::storage] pub type ClaimedRewards = StorageDoubleMap< _, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, WeakBoundedVec>, ValueQuery, >; /// Exposure of validator at era with the preferences of validators. /// /// This is keyed first by the era index to allow bulk deletion and then the stash account. /// /// Is it removed after [`Config::HistoryDepth`] eras. // If prefs hasn't been set or has been removed then 0 commission is returned. #[pezpallet::storage] pub type ErasValidatorPrefs = StorageDoubleMap< _, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, ValidatorPrefs, ValueQuery, >; /// The total validator era payout for the last [`Config::HistoryDepth`] eras. /// /// Eras that haven't finished yet or has been removed doesn't have reward. #[pezpallet::storage] pub type ErasValidatorReward = StorageMap<_, Twox64Concat, EraIndex, BalanceOf>; /// Rewards for the last [`Config::HistoryDepth`] eras. /// If reward hasn't been set or has been removed then 0 reward is returned. #[pezpallet::storage] pub type ErasRewardPoints = StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints, ValueQuery>; /// The total amount staked for the last [`Config::HistoryDepth`] eras. /// If total hasn't been set or has been removed then 0 stake is returned. #[pezpallet::storage] pub type ErasTotalStake = StorageMap<_, Twox64Concat, EraIndex, BalanceOf, ValueQuery>; /// Mode of era forcing. #[pezpallet::storage] pub type ForceEra = StorageValue<_, Forcing, ValueQuery>; /// Maximum staked rewards, i.e. the percentage of the era inflation that /// is used for stake rewards. /// See [Era payout](./index.html#era-payout). #[pezpallet::storage] pub type MaxStakedRewards = StorageValue<_, Percent, OptionQuery>; /// The percentage of the slash that is distributed to reporters. /// /// The rest of the slashed value is handled by the `Slash`. #[pezpallet::storage] pub type SlashRewardFraction = StorageValue<_, Perbill, ValueQuery>; /// The amount of currency given to reporters of a slash event which was /// canceled by extraordinary circumstances (e.g. governance). #[pezpallet::storage] pub type CanceledSlashPayout = StorageValue<_, BalanceOf, ValueQuery>; /// Stores reported offences in a queue until they are processed in subsequent blocks. /// /// Each offence is recorded under the corresponding era index and the offending validator's /// account. If an offence spans multiple pages, only one page is processed at a time. Offences /// are handled sequentially, with their associated slashes computed and stored in /// `UnappliedSlashes`. These slashes are then applied in a future era as determined by /// `SlashDeferDuration`. /// /// Any offences tied to an era older than `BondingDuration` are automatically dropped. /// Processing always prioritizes the oldest era first. #[pezpallet::storage] pub type OffenceQueue = StorageDoubleMap< _, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, slashing::OffenceRecord, >; /// Tracks the eras that contain offences in `OffenceQueue`, sorted from **earliest to latest**. /// /// - This ensures efficient retrieval of the oldest offence without iterating through /// `OffenceQueue`. /// - When a new offence is added to `OffenceQueue`, its era is **inserted in sorted order** /// if not already present. /// - When all offences for an era are processed, it is **removed** from this list. /// - The maximum length of this vector is bounded by `BondingDuration`. /// /// This eliminates the need for expensive iteration and sorting when fetching the next offence /// to process. #[pezpallet::storage] pub type OffenceQueueEras = StorageValue<_, WeakBoundedVec>; /// Tracks the currently processed offence record from the `OffenceQueue`. /// /// - When processing offences, an offence record is **popped** from the oldest era in /// `OffenceQueue` and stored here. /// - The function `process_offence` reads from this storage, processing one page of exposure at /// a time. /// - After processing a page, the `exposure_page` count is **decremented** until it reaches /// zero. /// - Once fully processed, the offence record is removed from this storage. /// /// This ensures that offences are processed incrementally, preventing excessive computation /// in a single block while maintaining correct slashing behavior. #[pezpallet::storage] pub type ProcessingOffence = StorageValue<_, (EraIndex, T::AccountId, slashing::OffenceRecord)>; /// All unapplied slashes that are queued for later. #[pezpallet::storage] pub type UnappliedSlashes = StorageDoubleMap< _, Twox64Concat, EraIndex, Twox64Concat, // Unique key for unapplied slashes: (validator, slash fraction, page index). (T::AccountId, Perbill, u32), UnappliedSlash, OptionQuery, >; /// Cancelled slashes by era and validator with maximum slash fraction to be cancelled. /// /// When slashes are cancelled by governance, this stores the era and the validators /// whose slashes should be cancelled, along with the maximum slash fraction that should /// be cancelled for each validator. #[pezpallet::storage] pub type CancelledSlashes = StorageMap< _, Twox64Concat, EraIndex, BoundedVec<(T::AccountId, Perbill), T::MaxValidatorSet>, ValueQuery, >; /// All slashing events on validators, mapped by era to the highest slash proportion /// and slash value of the era. #[pezpallet::storage] pub type ValidatorSlashInEra = StorageDoubleMap< _, Twox64Concat, EraIndex, Twox64Concat, T::AccountId, (Perbill, BalanceOf), >; /// The threshold for when users can start calling `chill_other` for other validators / /// nominators. The threshold is compared to the actual number of validators / nominators /// (`CountFor*`) in the system compared to the configured max (`Max*Count`). #[pezpallet::storage] pub type ChillThreshold = StorageValue<_, Percent, OptionQuery>; /// Voter snapshot progress status. /// /// If the status is `Ongoing`, it keeps a cursor of the last voter retrieved to proceed when /// creating the next snapshot page. #[pezpallet::storage] pub type VoterSnapshotStatus = StorageValue<_, SnapshotStatus, ValueQuery>; /// Keeps track of an ongoing multi-page election solution request. /// /// If `Some(_)``, it is the next page that we intend to elect. If `None`, we are not in the /// election process. /// /// This is only set in multi-block elections. Should always be `None` otherwise. #[pezpallet::storage] pub type NextElectionPage = StorageValue<_, PageIndex, OptionQuery>; /// A bounded list of the "electable" stashes that resulted from a successful election. #[pezpallet::storage] pub type ElectableStashes = StorageValue<_, BoundedBTreeSet, ValueQuery>; /// Tracks the current step of era pruning process for each era being lazily pruned. #[pezpallet::storage] pub type EraPruningState = StorageMap<_, Twox64Concat, EraIndex, PruningStep>; #[pezpallet::genesis_config] #[derive(pezframe_support::DefaultNoBound, pezframe_support::DebugNoBound)] pub struct GenesisConfig { pub validator_count: u32, pub invulnerables: BoundedVec, pub force_era: Forcing, pub slash_reward_fraction: Perbill, pub canceled_payout: BalanceOf, pub stakers: Vec<(T::AccountId, BalanceOf, crate::StakerStatus)>, pub min_nominator_bond: BalanceOf, pub min_validator_bond: BalanceOf, pub max_validator_count: Option, pub max_nominator_count: Option, /// Create the given number of validators and nominators. /// /// These account need not be in the endowment list of balances, and are auto-topped up /// here. /// /// Useful for testing genesis config. pub dev_stakers: Option<(u32, u32)>, /// initial active era, corresponding session index and start timestamp. pub active_era: (u32, u32, u64), } impl GenesisConfig { fn generate_endowed_bonded_account(derivation: &str, rng: &mut ChaChaRng) -> T::AccountId { let pair: SrPair = Pair::from_string(&derivation, None) .expect(&format!("Failed to parse derivation string: {derivation}")); let who = T::AccountId::decode(&mut &pair.public().encode()[..]) .expect(&format!("Failed to decode public key from pair: {:?}", pair.public())); let (min, max) = T::VoterList::range(); let stake = BalanceOf::::from(rng.next_u64().min(max).max(min)); let two: BalanceOf = 2u32.into(); assert_ok!(T::Currency::mint_into(&who, stake * two)); assert_ok!(>::bond( T::RuntimeOrigin::from(Some(who.clone()).into()), stake, RewardDestination::Staked, )); who } } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { crate::log!(trace, "initializing with {:?}", self); assert!( self.validator_count <= ::MaxWinnersPerPage::get() * ::Pages::get(), "validator count is too high, `ElectionProvider` can never fulfill this" ); ValidatorCount::::put(self.validator_count); assert!( self.invulnerables.len() as u32 <= T::MaxInvulnerables::get(), "Too many invulnerable validators at genesis." ); >::put(&self.invulnerables); ForceEra::::put(self.force_era); CanceledSlashPayout::::put(self.canceled_payout); SlashRewardFraction::::put(self.slash_reward_fraction); MinNominatorBond::::put(self.min_nominator_bond); MinValidatorBond::::put(self.min_validator_bond); if let Some(x) = self.max_validator_count { MaxValidatorsCount::::put(x); } if let Some(x) = self.max_nominator_count { MaxNominatorsCount::::put(x); } // First pass: set up all validators and idle stakers for &(ref stash, balance, ref status) in &self.stakers { match status { crate::StakerStatus::Validator => { crate::log!( trace, "inserting genesis validator: {:?} => {:?} => {:?}", stash, balance, status ); assert!( asset::free_to_stake::(stash) >= balance, "Stash does not have enough balance to bond." ); assert_ok!(>::bond( T::RuntimeOrigin::from(Some(stash.clone()).into()), balance, RewardDestination::Staked, )); assert_ok!(>::validate( T::RuntimeOrigin::from(Some(stash.clone()).into()), Default::default(), )); }, crate::StakerStatus::Idle => { crate::log!( trace, "inserting genesis idle staker: {:?} => {:?} => {:?}", stash, balance, status ); assert!( asset::free_to_stake::(stash) >= balance, "Stash does not have enough balance to bond." ); assert_ok!(>::bond( T::RuntimeOrigin::from(Some(stash.clone()).into()), balance, RewardDestination::Staked, )); }, _ => {}, } } // Second pass: set up all nominators (now that validators exist) for &(ref stash, balance, ref status) in &self.stakers { match status { crate::StakerStatus::Nominator(votes) => { crate::log!( trace, "inserting genesis nominator: {:?} => {:?} => {:?}", stash, balance, status ); assert!( asset::free_to_stake::(stash) >= balance, "Stash does not have enough balance to bond." ); assert_ok!(>::bond( T::RuntimeOrigin::from(Some(stash.clone()).into()), balance, RewardDestination::Staked, )); assert_ok!(>::nominate( T::RuntimeOrigin::from(Some(stash.clone()).into()), votes.iter().map(|l| T::Lookup::unlookup(l.clone())).collect(), )); }, _ => {}, } } // all voters are reported to the `VoterList`. assert_eq!( T::VoterList::count(), Nominators::::count() + Validators::::count(), "not all genesis stakers were inserted into sorted list provider, something is wrong." ); // now generate the dev stakers, after all else is setup if let Some((validators, nominators)) = self.dev_stakers { crate::log!( debug, "generating dev stakers: validators: {}, nominators: {}", validators, nominators ); let base_derivation = "//staker//{}"; // it is okay for the randomness to be the same on every call. If we want different, // we can make `base_derivation` configurable. let mut rng = ChaChaRng::from_seed(base_derivation.using_encoded(pezsp_core::blake2_256)); (0..validators).for_each(|index| { let derivation = base_derivation.replace("{}", &format!("validator{}", index)); let who = Self::generate_endowed_bonded_account(&derivation, &mut rng); assert_ok!(>::validate( T::RuntimeOrigin::from(Some(who.clone()).into()), Default::default(), )); }); // This allows us to work with configs like `dev_stakers: (0, 10)`. Don't create new // validators, just add a bunch of nominators. Useful for slashing tests. let all_validators = Validators::::iter_keys().collect::>(); (0..nominators).for_each(|index| { let derivation = base_derivation.replace("{}", &format!("nominator{}", index)); let who = Self::generate_endowed_bonded_account(&derivation, &mut rng); let random_nominations = all_validators .choose_multiple(&mut rng, MaxNominationsOf::::get() as usize) .map(|v| v.clone()) .collect::>(); assert_ok!(>::nominate( T::RuntimeOrigin::from(Some(who.clone()).into()), random_nominations.iter().map(|l| T::Lookup::unlookup(l.clone())).collect(), )); }) } let (active_era, session_index, timestamp) = self.active_era; ActiveEra::::put(ActiveEraInfo { index: active_era, start: Some(timestamp) }); // at genesis, we do not have any new planned era. CurrentEra::::put(active_era); // set the bonded genesis era BondedEras::::put( BoundedVec::<_, BondedErasBound>::try_from( alloc::vec![(active_era, session_index)] ) .expect("bound for BondedEras is BondingDuration + 1; can contain at least one element; qed") ); } } #[pezpallet::event] #[pezpallet::generate_deposit(pub fn deposit_event)] pub enum Event { /// The era payout has been set; the first balance is the validator-payout; the second is /// the remainder from the maximum amount of reward. EraPaid { era_index: EraIndex, validator_payout: BalanceOf, remainder: BalanceOf, }, /// The nominator has been rewarded by this amount to this destination. Rewarded { stash: T::AccountId, dest: RewardDestination, amount: BalanceOf, }, /// A staker (validator or nominator) has been slashed by the given amount. Slashed { staker: T::AccountId, amount: BalanceOf, }, /// An old slashing report from a prior era was discarded because it could /// not be processed. OldSlashingReportDiscarded { session_index: SessionIndex, }, /// An account has bonded this amount. \[stash, amount\] /// /// NOTE: This event is only emitted when funds are bonded via a dispatchable. Notably, /// it will not be emitted for staking rewards when they are added to stake. Bonded { stash: T::AccountId, amount: BalanceOf, }, /// An account has unbonded this amount. Unbonded { stash: T::AccountId, amount: BalanceOf, }, /// An account has called `withdraw_unbonded` and removed unbonding chunks worth `Balance` /// from the unlocking queue. Withdrawn { stash: T::AccountId, amount: BalanceOf, }, /// A subsequent event of `Withdrawn`, indicating that `stash` was fully removed from the /// system. StakerRemoved { stash: T::AccountId, }, /// A nominator has been kicked from a validator. Kicked { nominator: T::AccountId, stash: T::AccountId, }, /// An account has stopped participating as either a validator or nominator. Chilled { stash: T::AccountId, }, /// A Page of stakers rewards are getting paid. `next` is `None` if all pages are claimed. PayoutStarted { era_index: EraIndex, validator_stash: T::AccountId, page: Page, next: Option, }, /// A validator has set their preferences. ValidatorPrefsSet { stash: T::AccountId, prefs: ValidatorPrefs, }, /// Voters size limit reached. SnapshotVotersSizeExceeded { size: u32, }, /// Targets size limit reached. SnapshotTargetsSizeExceeded { size: u32, }, ForceEra { mode: Forcing, }, /// Report of a controller batch deprecation. ControllerBatchDeprecated { failures: u32, }, /// Staking balance migrated from locks to holds, with any balance that could not be held /// is force withdrawn. CurrencyMigrated { stash: T::AccountId, force_withdraw: BalanceOf, }, /// A page from a multi-page election was fetched. A number of these are followed by /// `StakersElected`. /// /// `Ok(count)` indicates the give number of stashes were added. /// `Err(index)` indicates that the stashes after index were dropped. /// `Err(0)` indicates that an error happened but no stashes were dropped nor added. /// /// The error indicates that a number of validators were dropped due to excess size, but /// the overall election will continue. PagedElectionProceeded { page: PageIndex, result: Result, }, /// An offence for the given validator, for the given percentage of their stake, at the /// given era as been reported. OffenceReported { offence_era: EraIndex, validator: T::AccountId, fraction: Perbill, }, /// An offence has been processed and the corresponding slash has been computed. SlashComputed { offence_era: EraIndex, slash_era: EraIndex, offender: T::AccountId, page: u32, }, /// An unapplied slash has been cancelled. SlashCancelled { slash_era: EraIndex, validator: T::AccountId, }, /// Session change has been triggered. /// /// If planned_era is one era ahead of active_era, it implies new era is being planned and /// election is ongoing. SessionRotated { starting_session: SessionIndex, active_era: EraIndex, planned_era: EraIndex, }, /// Something occurred that should never happen under normal operation. /// Logged as an event for fail-safe observability. Unexpected(UnexpectedKind), /// An offence was reported that was too old to be processed, and thus was dropped. OffenceTooOld { offence_era: EraIndex, validator: T::AccountId, fraction: Perbill, }, /// An old era with the given index was pruned. EraPruned { index: EraIndex, }, } /// Represents unexpected or invariant-breaking conditions encountered during execution. /// /// These variants are emitted as [`Event::Unexpected`] and indicate a defensive check has /// failed. While these should never occur under normal operation, they are useful for /// diagnosing issues in production or test environments. #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)] pub enum UnexpectedKind { /// Emitted when calculated era duration exceeds the configured maximum. EraDurationBoundExceeded, /// Received a validator activation event that is not recognized. UnknownValidatorActivation, } #[pezpallet::error] #[derive(PartialEq)] pub enum Error { /// Not a controller account. NotController, /// Not a stash account. NotStash, /// Stash is already bonded. AlreadyBonded, /// Controller is already paired. AlreadyPaired, /// Targets cannot be empty. EmptyTargets, /// Duplicate index. DuplicateIndex, /// Slash record not found. InvalidSlashRecord, /// Cannot bond, nominate or validate with value less than the minimum defined by /// governance (see `MinValidatorBond` and `MinNominatorBond`). If unbonding is the /// intention, `chill` first to remove one's role as validator/nominator. InsufficientBond, /// Can not schedule more unlock chunks. NoMoreChunks, /// Can not rebond without unlocking chunks. NoUnlockChunk, /// Attempting to target a stash that still has funds. FundedTarget, /// Invalid era to reward. InvalidEraToReward, /// Invalid number of nominations. InvalidNumberOfNominations, /// Rewards for this era have already been claimed for this validator. AlreadyClaimed, /// No nominators exist on this page. InvalidPage, /// Incorrect previous history depth input provided. IncorrectHistoryDepth, /// Internal state has become somehow corrupted and the operation cannot continue. BadState, /// Too many nomination targets supplied. TooManyTargets, /// A nomination target was supplied that was blocked or otherwise not a validator. BadTarget, /// The user has enough bond and thus cannot be chilled forcefully by an external person. CannotChillOther, /// There are too many nominators in the system. Governance needs to adjust the staking /// settings to keep things safe for the runtime. TooManyNominators, /// There are too many validator candidates in the system. Governance needs to adjust the /// staking settings to keep things safe for the runtime. TooManyValidators, /// Commission is too low. Must be at least `MinCommission`. CommissionTooLow, /// Some bound is not met. BoundNotMet, /// Used when attempting to use deprecated controller account logic. ControllerDeprecated, /// Cannot reset a ledger. CannotRestoreLedger, /// Provided reward destination is not allowed. RewardDestinationRestricted, /// Not enough funds available to withdraw. NotEnoughFunds, /// Operation not allowed for virtual stakers. VirtualStakerNotAllowed, /// Stash could not be reaped as other pezpallet might depend on it. CannotReapStash, /// The stake of this account is already migrated to `Fungible` holds. AlreadyMigrated, /// Era not yet started. EraNotStarted, /// Account is restricted from participation in staking. This may happen if the account is /// staking in another way already, such as via pool. Restricted, /// Unapplied slashes in the recently concluded era is blocking this operation. /// See `Call::apply_slash` to apply them. UnappliedSlashesInPreviousEra, /// The era is not eligible for pruning. EraNotPrunable, /// The slash has been cancelled and cannot be applied. CancelledSlash, } impl Pezpallet { /// Apply previously-unapplied slashes on the beginning of a new era, after a delay. pub fn apply_unapplied_slashes(active_era: EraIndex) -> Weight { let mut slashes = UnappliedSlashes::::iter_prefix(&active_era).take(1); if let Some((key, slash)) = slashes.next() { crate::log!( debug, "🦹 found slash {:?} scheduled to be executed in era {:?}", slash, active_era, ); // Check if this slash has been cancelled if Self::check_slash_cancelled(active_era, &key.0, key.1) { crate::log!( debug, "🦹 slash for {:?} in era {:?} was cancelled, skipping", key.0, active_era, ); } else { let offence_era = active_era.saturating_sub(T::SlashDeferDuration::get()); slashing::apply_slash::(slash, offence_era); } // Always remove the slash from UnappliedSlashes UnappliedSlashes::::remove(&active_era, &key); // Check if there are more slashes for this era if UnappliedSlashes::::iter_prefix(&active_era).next().is_none() { // No more slashes for this era, clear CancelledSlashes CancelledSlashes::::remove(&active_era); } T::WeightInfo::apply_slash() } else { // No slashes found for this era T::DbWeight::get().reads(1) } } /// Execute one step of era pruning and get actual weight used fn do_prune_era_step(era: EraIndex) -> Result { // Get current pruning state. If EraPruningState doesn't exist, it means: // - Era was never marked for pruning, OR // - Era was already fully pruned (pruning state was removed on final step) // In either case, this is an error - user should not call prune on non-prunable eras let current_step = EraPruningState::::get(era).ok_or(Error::::EraNotPrunable)?; // Limit items to prevent deleting more than we can safely account for in weight // calculations let items_limit = T::MaxPruningItems::get().min(T::MaxValidatorSet::get()); let actual_weight = match current_step { PruningStep::ErasStakersPaged => { let result = ErasStakersPaged::::clear_prefix((era,), items_limit, None); let items_deleted = result.backend as u32; result.maybe_cursor.is_none().then(|| { EraPruningState::::insert(era, PruningStep::ErasStakersOverview) }); T::WeightInfo::prune_era_stakers_paged(items_deleted) }, PruningStep::ErasStakersOverview => { let result = ErasStakersOverview::::clear_prefix(era, items_limit, None); let items_deleted = result.backend as u32; result.maybe_cursor.is_none().then(|| { EraPruningState::::insert(era, PruningStep::ErasValidatorPrefs) }); T::WeightInfo::prune_era_stakers_overview(items_deleted) }, PruningStep::ErasValidatorPrefs => { let result = ErasValidatorPrefs::::clear_prefix(era, items_limit, None); let items_deleted = result.backend as u32; result .maybe_cursor .is_none() .then(|| EraPruningState::::insert(era, PruningStep::ClaimedRewards)); T::WeightInfo::prune_era_validator_prefs(items_deleted) }, PruningStep::ClaimedRewards => { let result = ClaimedRewards::::clear_prefix(era, items_limit, None); let items_deleted = result.backend as u32; result.maybe_cursor.is_none().then(|| { EraPruningState::::insert(era, PruningStep::ErasValidatorReward) }); T::WeightInfo::prune_era_claimed_rewards(items_deleted) }, PruningStep::ErasValidatorReward => { ErasValidatorReward::::remove(era); EraPruningState::::insert(era, PruningStep::ErasRewardPoints); T::WeightInfo::prune_era_validator_reward() }, PruningStep::ErasRewardPoints => { ErasRewardPoints::::remove(era); EraPruningState::::insert(era, PruningStep::ErasTotalStake); T::WeightInfo::prune_era_reward_points() }, PruningStep::ErasTotalStake => { ErasTotalStake::::remove(era); // This is the final step - remove the pruning state EraPruningState::::remove(era); T::WeightInfo::prune_era_total_stake() }, }; // Check if era is fully pruned (pruning state removed) and emit event if EraPruningState::::get(era).is_none() { Self::deposit_event(Event::::EraPruned { index: era }); } Ok(actual_weight) } } #[pezpallet::hooks] impl Hooks> for Pezpallet { fn on_initialize(_now: BlockNumberFor) -> Weight { // process our queue. let mut consumed_weight = slashing::process_offence::(); // apply any pending slashes after `SlashDeferDuration`. consumed_weight.saturating_accrue(T::DbWeight::get().reads(1)); if let Some(active_era) = ActiveEra::::get() { let slash_weight = Self::apply_unapplied_slashes(active_era.index); consumed_weight.saturating_accrue(slash_weight); } // maybe plan eras and stuff. Note that this is benchmark as a part of the // election-provider's benchmarks. session_rotation::EraElectionPlanner::::maybe_fetch_election_results(); consumed_weight } fn integrity_test() { // ensure that we funnel the correct value to the `DataProvider::MaxVotesPerVoter`; assert_eq!( MaxNominationsOf::::get(), ::MaxVotesPerVoter::get() ); // and that MaxNominations is always greater than 1, since we count on this. assert!(!MaxNominationsOf::::get().is_zero()); assert!( T::SlashDeferDuration::get() < T::BondingDuration::get() || T::BondingDuration::get() == 0, "As per documentation, slash defer duration ({}) should be less than bonding duration ({}).", T::SlashDeferDuration::get(), T::BondingDuration::get(), ); // Ensure MaxPruningItems is reasonable (minimum 100 for efficiency) assert!( T::MaxPruningItems::get() >= 100, "MaxPruningItems must be at least 100 for efficient pruning, got: {}", T::MaxPruningItems::get() ); } #[cfg(feature = "try-runtime")] fn try_state(n: BlockNumberFor) -> Result<(), pezsp_runtime::TryRuntimeError> { Self::do_try_state(n) } } #[pezpallet::call] impl Pezpallet { /// Take the origin account as a stash and lock up `value` of its balance. `controller` will /// be the account that controls it. /// /// `value` must be more than the `minimum_balance` specified by `T::Currency`. /// /// The dispatch origin for this call must be _Signed_ by the stash account. /// /// Emits `Bonded`. /// /// NOTE: Two of the storage writes (`Self::bonded`, `Self::payee`) are _never_ cleaned /// unless the `origin` falls below _existential deposit_ (or equal to 0) and gets removed /// as dust. #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::bond())] pub fn bond( origin: OriginFor, #[pezpallet::compact] value: BalanceOf, payee: RewardDestination, ) -> DispatchResult { let stash = ensure_signed(origin)?; ensure!(!T::Filter::contains(&stash), Error::::Restricted); if StakingLedger::::is_bonded(StakingAccount::Stash(stash.clone())) { return Err(Error::::AlreadyBonded.into()); } // An existing controller cannot become a stash. if StakingLedger::::is_bonded(StakingAccount::Controller(stash.clone())) { return Err(Error::::AlreadyPaired.into()); } // Reject a bond which is lower than the minimum bond. if value < Self::min_chilled_bond() { return Err(Error::::InsufficientBond.into()); } let stash_balance = asset::free_to_stake::(&stash); let value = value.min(stash_balance); Self::deposit_event(Event::::Bonded { stash: stash.clone(), amount: value }); let ledger = StakingLedger::::new(stash.clone(), value); // You're auto-bonded forever, here. We might improve this by only bonding when // you actually validate/nominate and remove once you unbond __everything__. ledger.bond(payee)?; Ok(()) } /// Add some extra amount that have appeared in the stash `free_balance` into the balance up /// for staking. /// /// The dispatch origin for this call must be _Signed_ by the stash, not the controller. /// /// Use this if there are additional funds in your stash account that you wish to bond. /// Unlike [`bond`](Self::bond) or [`unbond`](Self::unbond) this function does not impose /// any limitation on the amount that can be added. /// /// Emits `Bonded`. #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::bond_extra())] pub fn bond_extra( origin: OriginFor, #[pezpallet::compact] max_additional: BalanceOf, ) -> DispatchResult { let stash = ensure_signed(origin)?; ensure!(!T::Filter::contains(&stash), Error::::Restricted); Self::do_bond_extra(&stash, max_additional) } /// Schedule a portion of the stash to be unlocked ready for transfer out after the bond /// period ends. If this leaves an amount actively bonded less than /// [`asset::existential_deposit`], then it is increased to the full amount. /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. /// /// Once the unlock period is done, you can call `withdraw_unbonded` to actually move /// the funds out of management ready for transfer. /// /// No more than a limited number of unlocking chunks (see `MaxUnlockingChunks`) /// can co-exists at the same time. If there are no unlocking chunks slots available /// [`Call::withdraw_unbonded`] is called to remove some of the chunks (if possible). /// /// If a user encounters the `InsufficientBond` error when calling this extrinsic, /// they should call `chill` first in order to free up their bonded funds. /// /// Emits `Unbonded`. /// /// See also [`Call::withdraw_unbonded`]. #[pezpallet::call_index(2)] #[pezpallet::weight( T::WeightInfo::withdraw_unbonded_kill().saturating_add(T::WeightInfo::unbond())) ] pub fn unbond( origin: OriginFor, #[pezpallet::compact] value: BalanceOf, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; let unlocking = Self::ledger(Controller(controller.clone())).map(|l| l.unlocking.len())?; // if there are no unlocking chunks available, try to remove any chunks by withdrawing // funds that have fully unbonded. let maybe_withdraw_weight = { if unlocking == T::MaxUnlockingChunks::get() as usize { Some(Self::do_withdraw_unbonded(&controller)?) } else { None } }; // we need to fetch the ledger again because it may have been mutated in the call // to `Self::do_withdraw_unbonded` above. let mut ledger = Self::ledger(Controller(controller))?; let mut value = value.min(ledger.active); let stash = ledger.stash.clone(); ensure!( ledger.unlocking.len() < T::MaxUnlockingChunks::get() as usize, Error::::NoMoreChunks, ); if !value.is_zero() { ledger.active -= value; // Avoid there being a dust balance left in the staking system. if ledger.active < asset::existential_deposit::() { value += ledger.active; ledger.active = Zero::zero(); } let min_active_bond = if Nominators::::contains_key(&stash) { Self::min_nominator_bond() } else if Validators::::contains_key(&stash) { Self::min_validator_bond() } else { // staker is chilled, no min bond. Zero::zero() }; // Make sure that the user maintains enough active bond for their role. // If a user runs into this error, they should chill first. ensure!(ledger.active >= min_active_bond, Error::::InsufficientBond); // Note: we used current era before, but that is meant to be used for only election. // The right value to use here is the active era. let era = session_rotation::Rotator::::active_era() .saturating_add(T::BondingDuration::get()); if let Some(chunk) = ledger.unlocking.last_mut().filter(|chunk| chunk.era == era) { // To keep the chunk count down, we only keep one chunk per era. Since // `unlocking` is a FiFo queue, if a chunk exists for `era` we know that it will // be the last one. chunk.value = chunk.value.defensive_saturating_add(value) } else { ledger .unlocking .try_push(UnlockChunk { value, era }) .map_err(|_| Error::::NoMoreChunks)?; }; // NOTE: ledger must be updated prior to calling `Self::weight_of`. ledger.update()?; // update this staker in the sorted list, if they exist in it. if T::VoterList::contains(&stash) { let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)); } Self::deposit_event(Event::::Unbonded { stash, amount: value }); } let actual_weight = if let Some(withdraw_weight) = maybe_withdraw_weight { Some(T::WeightInfo::unbond().saturating_add(withdraw_weight)) } else { Some(T::WeightInfo::unbond()) }; Ok(actual_weight.into()) } /// Remove any stake that has been fully unbonded and is ready for withdrawal. /// /// Stake is considered fully unbonded once [`Config::BondingDuration`] has elapsed since /// the unbonding was initiated. In rare cases—such as when offences for the unbonded era /// have been reported but not yet processed—withdrawal is restricted to eras for which /// all offences have been processed. /// /// The unlocked stake will be returned as free balance in the stash account. /// /// The dispatch origin for this call must be _Signed_ by the controller. /// /// Emits `Withdrawn`. /// /// See also [`Call::unbond`]. /// /// ## Parameters /// /// - `num_slashing_spans`: **Deprecated**. Retained only for backward compatibility; this /// parameter has no effect. #[pezpallet::call_index(3)] #[pezpallet::weight(T::WeightInfo::withdraw_unbonded_kill())] pub fn withdraw_unbonded( origin: OriginFor, _num_slashing_spans: u32, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; let actual_weight = Self::do_withdraw_unbonded(&controller)?; Ok(Some(actual_weight).into()) } /// Declare the desire to validate for the origin controller. /// /// Effects will be felt at the beginning of the next era. /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. #[pezpallet::call_index(4)] #[pezpallet::weight(T::WeightInfo::validate())] pub fn validate(origin: OriginFor, prefs: ValidatorPrefs) -> DispatchResult { let controller = ensure_signed(origin)?; let ledger = Self::ledger(Controller(controller))?; ensure!(ledger.active >= Self::min_validator_bond(), Error::::InsufficientBond); let stash = &ledger.stash; // ensure their commission is correct. ensure!(prefs.commission >= MinCommission::::get(), Error::::CommissionTooLow); // Only check limits if they are not already a validator. if !Validators::::contains_key(stash) { // If this error is reached, we need to adjust the `MinValidatorBond` and start // calling `chill_other`. Until then, we explicitly block new validators to protect // the runtime. if let Some(max_validators) = MaxValidatorsCount::::get() { ensure!( Validators::::count() < max_validators, Error::::TooManyValidators ); } } Self::do_remove_nominator(stash); Self::do_add_validator(stash, prefs.clone()); Self::deposit_event(Event::::ValidatorPrefsSet { stash: ledger.stash, prefs }); Ok(()) } /// Declare the desire to nominate `targets` for the origin controller. /// /// Effects will be felt at the beginning of the next era. /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. #[pezpallet::call_index(5)] #[pezpallet::weight(T::WeightInfo::nominate(targets.len() as u32))] pub fn nominate( origin: OriginFor, targets: Vec>, ) -> DispatchResult { let controller = ensure_signed(origin)?; let ledger = Self::ledger(StakingAccount::Controller(controller.clone()))?; ensure!(ledger.active >= Self::min_nominator_bond(), Error::::InsufficientBond); let stash = &ledger.stash; // Only check limits if they are not already a nominator. if !Nominators::::contains_key(stash) { // If this error is reached, we need to adjust the `MinNominatorBond` and start // calling `chill_other`. Until then, we explicitly block new nominators to protect // the runtime. if let Some(max_nominators) = MaxNominatorsCount::::get() { ensure!( Nominators::::count() < max_nominators, Error::::TooManyNominators ); } } // dedup targets let mut targets = targets .into_iter() .map(|t| T::Lookup::lookup(t).map_err(DispatchError::from)) .collect::, _>>()?; targets.sort(); targets.dedup(); ensure!(!targets.is_empty(), Error::::EmptyTargets); ensure!( targets.len() <= T::NominationsQuota::get_quota(ledger.active) as usize, Error::::TooManyTargets ); let old = Nominators::::get(stash).map_or_else(Vec::new, |x| x.targets.into_inner()); let targets: BoundedVec<_, _> = targets .into_iter() .map(|n| { if old.contains(&n) || (Validators::::contains_key(&n) && !Validators::::get(&n).blocked) { Ok(n) } else { Err(Error::::BadTarget.into()) } }) .collect::, DispatchError>>()? .try_into() .map_err(|_| Error::::TooManyNominators)?; let nominations = Nominations { targets, // Initial nominations are considered submitted at era 0. See `Nominations` doc. submitted_in: CurrentEra::::get().unwrap_or(0), suppressed: false, }; Self::do_remove_validator(stash); Self::do_add_nominator(stash, nominations); Ok(()) } /// Declare no desire to either validate or nominate. /// /// Effects will be felt at the beginning of the next era. /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. /// /// ## Complexity /// - Independent of the arguments. Insignificant complexity. /// - Contains one read. /// - Writes are limited to the `origin` account key. #[pezpallet::call_index(6)] #[pezpallet::weight(T::WeightInfo::chill())] pub fn chill(origin: OriginFor) -> DispatchResult { let controller = ensure_signed(origin)?; let ledger = Self::ledger(StakingAccount::Controller(controller))?; Self::chill_stash(&ledger.stash); Ok(()) } /// (Re-)set the payment target for a controller. /// /// Effects will be felt instantly (as soon as this function is completed successfully). /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. #[pezpallet::call_index(7)] #[pezpallet::weight(T::WeightInfo::set_payee())] pub fn set_payee( origin: OriginFor, payee: RewardDestination, ) -> DispatchResult { let controller = ensure_signed(origin)?; let ledger = Self::ledger(Controller(controller.clone()))?; ensure!( (payee != { #[allow(deprecated)] RewardDestination::Controller }), Error::::ControllerDeprecated ); let _ = ledger .set_payee(payee) .defensive_proof("ledger was retrieved from storage, thus it's bonded; qed.")?; Ok(()) } /// (Re-)sets the controller of a stash to the stash itself. This function previously /// accepted a `controller` argument to set the controller to an account other than the /// stash itself. This functionality has now been removed, now only setting the controller /// to the stash, if it is not already. /// /// Effects will be felt instantly (as soon as this function is completed successfully). /// /// The dispatch origin for this call must be _Signed_ by the stash, not the controller. #[pezpallet::call_index(8)] #[pezpallet::weight(T::WeightInfo::set_controller())] pub fn set_controller(origin: OriginFor) -> DispatchResult { let stash = ensure_signed(origin)?; Self::ledger(StakingAccount::Stash(stash.clone())).map(|ledger| { let controller = ledger.controller() .defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.") .ok_or(Error::::NotController)?; if controller == stash { // Stash is already its own controller. return Err(Error::::AlreadyPaired.into()) } let _ = ledger.set_controller_to_stash()?; Ok(()) })? } /// Sets the ideal number of validators. /// /// The dispatch origin must be Root. #[pezpallet::call_index(9)] #[pezpallet::weight(T::WeightInfo::set_validator_count())] pub fn set_validator_count( origin: OriginFor, #[pezpallet::compact] new: u32, ) -> DispatchResult { ensure_root(origin)?; ensure!(new <= T::MaxValidatorSet::get(), Error::::TooManyValidators); ValidatorCount::::put(new); Ok(()) } /// Increments the ideal number of validators up to maximum of /// `T::MaxValidatorSet`. /// /// The dispatch origin must be Root. #[pezpallet::call_index(10)] #[pezpallet::weight(T::WeightInfo::set_validator_count())] pub fn increase_validator_count( origin: OriginFor, #[pezpallet::compact] additional: u32, ) -> DispatchResult { ensure_root(origin)?; let old = ValidatorCount::::get(); let new = old.checked_add(additional).ok_or(ArithmeticError::Overflow)?; ensure!(new <= T::MaxValidatorSet::get(), Error::::TooManyValidators); ValidatorCount::::put(new); Ok(()) } /// Scale up the ideal number of validators by a factor up to maximum of /// `T::MaxValidatorSet`. /// /// The dispatch origin must be Root. #[pezpallet::call_index(11)] #[pezpallet::weight(T::WeightInfo::set_validator_count())] pub fn scale_validator_count(origin: OriginFor, factor: Percent) -> DispatchResult { ensure_root(origin)?; let old = ValidatorCount::::get(); let new = old.checked_add(factor.mul_floor(old)).ok_or(ArithmeticError::Overflow)?; ensure!(new <= T::MaxValidatorSet::get(), Error::::TooManyValidators); ValidatorCount::::put(new); Ok(()) } /// Force there to be no new eras indefinitely. /// /// The dispatch origin must be Root. /// /// # Warning /// /// The election process starts multiple blocks before the end of the era. /// Thus the election process may be ongoing when this is called. In this case the /// election will continue until the next era is triggered. #[pezpallet::call_index(12)] #[pezpallet::weight(T::WeightInfo::force_no_eras())] pub fn force_no_eras(origin: OriginFor) -> DispatchResult { ensure_root(origin)?; Self::set_force_era(Forcing::ForceNone); Ok(()) } /// Force there to be a new era at the end of the next session. After this, it will be /// reset to normal (non-forced) behaviour. /// /// The dispatch origin must be Root. /// /// # Warning /// /// The election process starts multiple blocks before the end of the era. /// If this is called just before a new era is triggered, the election process may not /// have enough blocks to get a result. #[pezpallet::call_index(13)] #[pezpallet::weight(T::WeightInfo::force_new_era())] pub fn force_new_era(origin: OriginFor) -> DispatchResult { ensure_root(origin)?; Self::set_force_era(Forcing::ForceNew); Ok(()) } /// Set the validators who cannot be slashed (if any). /// /// The dispatch origin must be Root. #[pezpallet::call_index(14)] #[pezpallet::weight(T::WeightInfo::set_invulnerables(invulnerables.len() as u32))] pub fn set_invulnerables( origin: OriginFor, invulnerables: Vec, ) -> DispatchResult { ensure_root(origin)?; let invulnerables = BoundedVec::try_from(invulnerables).map_err(|_| Error::::BoundNotMet)?; >::put(invulnerables); Ok(()) } /// Force a current staker to become completely unstaked, immediately. /// /// The dispatch origin must be Root. /// ## Parameters /// /// - `stash`: The stash account to be unstaked. /// - `num_slashing_spans`: **Deprecated**. This parameter is retained for backward /// compatibility. It no longer has any effect. #[pezpallet::call_index(15)] #[pezpallet::weight(T::WeightInfo::force_unstake())] pub fn force_unstake( origin: OriginFor, stash: T::AccountId, _num_slashing_spans: u32, ) -> DispatchResult { ensure_root(origin)?; // Remove all staking-related information and lock. Self::kill_stash(&stash)?; Ok(()) } /// Force there to be a new era at the end of sessions indefinitely. /// /// The dispatch origin must be Root. /// /// # Warning /// /// The election process starts multiple blocks before the end of the era. /// If this is called just before a new era is triggered, the election process may not /// have enough blocks to get a result. #[pezpallet::call_index(16)] #[pezpallet::weight(T::WeightInfo::force_new_era_always())] pub fn force_new_era_always(origin: OriginFor) -> DispatchResult { ensure_root(origin)?; Self::set_force_era(Forcing::ForceAlways); Ok(()) } /// Cancels scheduled slashes for a given era before they are applied. /// /// This function allows `T::AdminOrigin` to cancel pending slashes for specified validators /// in a given era. The cancelled slashes are stored and will be checked when applying /// slashes. /// /// ## Parameters /// - `era`: The staking era for which slashes should be cancelled. This is the era where /// the slash would be applied, not the era in which the offence was committed. /// - `validator_slashes`: A list of validator stash accounts and their slash fractions to /// be cancelled. #[pezpallet::call_index(17)] #[pezpallet::weight(T::WeightInfo::cancel_deferred_slash(validator_slashes.len() as u32))] pub fn cancel_deferred_slash( origin: OriginFor, era: EraIndex, validator_slashes: Vec<(T::AccountId, Perbill)>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; ensure!(!validator_slashes.is_empty(), Error::::EmptyTargets); // Get current cancelled slashes for this era let mut cancelled_slashes = CancelledSlashes::::get(&era); // Process each validator slash for (validator, slash_fraction) in validator_slashes { // Since this is gated by admin origin, we don't need to check if they are really // validators and trust governance to correctly set the parameters. // Remove any existing entry for this validator cancelled_slashes.retain(|(v, _)| v != &validator); // Add the validator with the specified slash fraction cancelled_slashes .try_push((validator.clone(), slash_fraction)) .map_err(|_| Error::::BoundNotMet) .defensive_proof("cancelled_slashes should have capacity for all validators")?; Self::deposit_event(Event::::SlashCancelled { slash_era: era, validator }); } // Update storage CancelledSlashes::::insert(&era, cancelled_slashes); Ok(()) } /// Pay out next page of the stakers behind a validator for the given era. /// /// - `validator_stash` is the stash account of the validator. /// - `era` may be any era between `[current_era - history_depth; current_era]`. /// /// The origin of this call must be _Signed_. Any account can call this function, even if /// it is not one of the stakers. /// /// The reward payout could be paged in case there are too many nominators backing the /// `validator_stash`. This call will payout unpaid pages in an ascending order. To claim a /// specific page, use `payout_stakers_by_page`.` /// /// If all pages are claimed, it returns an error `InvalidPage`. #[pezpallet::call_index(18)] #[pezpallet::weight(T::WeightInfo::payout_stakers_alive_staked( T::MaxExposurePageSize::get() ))] pub fn payout_stakers( origin: OriginFor, validator_stash: T::AccountId, era: EraIndex, ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; Self::do_payout_stakers(validator_stash, era) } /// Rebond a portion of the stash scheduled to be unlocked. /// /// The dispatch origin must be signed by the controller. #[pezpallet::call_index(19)] #[pezpallet::weight(T::WeightInfo::rebond(T::MaxUnlockingChunks::get() as u32))] pub fn rebond( origin: OriginFor, #[pezpallet::compact] value: BalanceOf, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; let ledger = Self::ledger(Controller(controller))?; ensure!(!T::Filter::contains(&ledger.stash), Error::::Restricted); ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockChunk); let initial_unlocking = ledger.unlocking.len() as u32; let (ledger, rebonded_value) = ledger.rebond(value); // Last check: the new active amount of ledger must be more than min bond. ensure!(ledger.active >= Self::min_chilled_bond(), Error::::InsufficientBond); Self::deposit_event(Event::::Bonded { stash: ledger.stash.clone(), amount: rebonded_value, }); let stash = ledger.stash.clone(); let final_unlocking = ledger.unlocking.len(); // NOTE: ledger must be updated prior to calling `Self::weight_of`. ledger.update()?; if T::VoterList::contains(&stash) { let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)); } let removed_chunks = 1u32 // for the case where the last iterated chunk is not removed .saturating_add(initial_unlocking) .saturating_sub(final_unlocking as u32); Ok(Some(T::WeightInfo::rebond(removed_chunks)).into()) } /// Remove all data structures concerning a staker/stash once it is at a state where it can /// be considered `dust` in the staking system. The requirements are: /// /// 1. the `total_balance` of the stash is below `min_chilled_bond` or is zero. /// 2. or, the `ledger.total` of the stash is below `min_chilled_bond` or is zero. /// /// The former can happen in cases like a slash; the latter when a fully unbonded account /// is still receiving staking rewards in `RewardDestination::Staked`. /// /// It can be called by anyone, as long as `stash` meets the above requirements. /// /// Refunds the transaction fees upon successful execution. /// /// ## Parameters /// /// - `stash`: The stash account to be reaped. /// - `num_slashing_spans`: **Deprecated**. This parameter is retained for backward /// compatibility. It no longer has any effect. #[pezpallet::call_index(20)] #[pezpallet::weight(T::WeightInfo::reap_stash())] pub fn reap_stash( origin: OriginFor, stash: T::AccountId, _num_slashing_spans: u32, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; // virtual stakers should not be allowed to be reaped. ensure!(!Self::is_virtual_staker(&stash), Error::::VirtualStakerNotAllowed); let min_chilled_bond = Self::min_chilled_bond(); let origin_balance = asset::total_balance::(&stash); let ledger_total = Self::ledger(Stash(stash.clone())).map(|l| l.total).unwrap_or_default(); let reapable = origin_balance < min_chilled_bond || origin_balance.is_zero() || ledger_total < min_chilled_bond || ledger_total.is_zero(); ensure!(reapable, Error::::FundedTarget); // Remove all staking-related information and lock. Self::kill_stash(&stash)?; Ok(Pays::No.into()) } /// Remove the given nominations from the calling validator. /// /// Effects will be felt at the beginning of the next era. /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. /// /// - `who`: A list of nominator stash accounts who are nominating this validator which /// should no longer be nominating this validator. /// /// Note: Making this call only makes sense if you first set the validator preferences to /// block any further nominations. #[pezpallet::call_index(21)] #[pezpallet::weight(T::WeightInfo::kick(who.len() as u32))] pub fn kick(origin: OriginFor, who: Vec>) -> DispatchResult { let controller = ensure_signed(origin)?; let ledger = Self::ledger(Controller(controller))?; let stash = &ledger.stash; for nom_stash in who .into_iter() .map(T::Lookup::lookup) .collect::, _>>()? .into_iter() { Nominators::::mutate(&nom_stash, |maybe_nom| { if let Some(ref mut nom) = maybe_nom { if let Some(pos) = nom.targets.iter().position(|v| v == stash) { nom.targets.swap_remove(pos); Self::deposit_event(Event::::Kicked { nominator: nom_stash.clone(), stash: stash.clone(), }); } } }); } Ok(()) } /// Update the various staking configurations . /// /// * `min_nominator_bond`: The minimum active bond needed to be a nominator. /// * `min_validator_bond`: The minimum active bond needed to be a validator. /// * `max_nominator_count`: The max number of users who can be a nominator at once. When /// set to `None`, no limit is enforced. /// * `max_validator_count`: The max number of users who can be a validator at once. When /// set to `None`, no limit is enforced. /// * `chill_threshold`: The ratio of `max_nominator_count` or `max_validator_count` which /// should be filled in order for the `chill_other` transaction to work. /// * `min_commission`: The minimum amount of commission that each validators must maintain. /// This is checked only upon calling `validate`. Existing validators are not affected. /// /// RuntimeOrigin must be Root to call this function. /// /// NOTE: Existing nominators and validators will not be affected by this update. /// to kick people under the new limits, `chill_other` should be called. // We assume the worst case for this call is either: all items are set or all items are // removed. #[pezpallet::call_index(22)] #[pezpallet::weight( T::WeightInfo::set_staking_configs_all_set() .max(T::WeightInfo::set_staking_configs_all_remove()) )] pub fn set_staking_configs( origin: OriginFor, min_nominator_bond: ConfigOp>, min_validator_bond: ConfigOp>, max_nominator_count: ConfigOp, max_validator_count: ConfigOp, chill_threshold: ConfigOp, min_commission: ConfigOp, max_staked_rewards: ConfigOp, ) -> DispatchResult { ensure_root(origin)?; macro_rules! config_op_exp { ($storage:ty, $op:ident) => { match $op { ConfigOp::Noop => (), ConfigOp::Set(v) => <$storage>::put(v), ConfigOp::Remove => <$storage>::kill(), } }; } config_op_exp!(MinNominatorBond, min_nominator_bond); config_op_exp!(MinValidatorBond, min_validator_bond); config_op_exp!(MaxNominatorsCount, max_nominator_count); config_op_exp!(MaxValidatorsCount, max_validator_count); config_op_exp!(ChillThreshold, chill_threshold); config_op_exp!(MinCommission, min_commission); config_op_exp!(MaxStakedRewards, max_staked_rewards); Ok(()) } /// Declare a `controller` to stop participating as either a validator or nominator. /// /// Effects will be felt at the beginning of the next era. /// /// The dispatch origin for this call must be _Signed_, but can be called by anyone. /// /// If the caller is the same as the controller being targeted, then no further checks are /// enforced, and this function behaves just like `chill`. /// /// If the caller is different than the controller being targeted, the following conditions /// must be met: /// /// * `controller` must belong to a nominator who has become non-decodable, /// /// Or: /// /// * A `ChillThreshold` must be set and checked which defines how close to the max /// nominators or validators we must reach before users can start chilling one-another. /// * A `MaxNominatorCount` and `MaxValidatorCount` must be set which is used to determine /// how close we are to the threshold. /// * A `MinNominatorBond` and `MinValidatorBond` must be set and checked, which determines /// if this is a person that should be chilled because they have not met the threshold /// bond required. /// /// This can be helpful if bond requirements are updated, and we need to remove old users /// who do not satisfy these requirements. #[pezpallet::call_index(23)] #[pezpallet::weight(T::WeightInfo::chill_other())] pub fn chill_other(origin: OriginFor, stash: T::AccountId) -> DispatchResult { // Anyone can call this function. let caller = ensure_signed(origin)?; let ledger = Self::ledger(Stash(stash.clone()))?; let controller = ledger .controller() .defensive_proof( "Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.", ) .ok_or(Error::::NotController)?; // In order for one user to chill another user, the following conditions must be met: // // * `controller` belongs to a nominator who has become non-decodable, // // Or // // * A `ChillThreshold` is set which defines how close to the max nominators or // validators we must reach before users can start chilling one-another. // * A `MaxNominatorCount` and `MaxValidatorCount` which is used to determine how close // we are to the threshold. // * A `MinNominatorBond` and `MinValidatorBond` which is the final condition checked to // determine this is a person that should be chilled because they have not met the // threshold bond required. // // Otherwise, if caller is the same as the controller, this is just like `chill`. if Nominators::::contains_key(&stash) && Nominators::::get(&stash).is_none() { Self::chill_stash(&stash); return Ok(()); } if caller != controller { let threshold = ChillThreshold::::get().ok_or(Error::::CannotChillOther)?; let min_active_bond = if Nominators::::contains_key(&stash) { let max_nominator_count = MaxNominatorsCount::::get().ok_or(Error::::CannotChillOther)?; let current_nominator_count = Nominators::::count(); ensure!( threshold * max_nominator_count < current_nominator_count, Error::::CannotChillOther ); Self::min_nominator_bond() } else if Validators::::contains_key(&stash) { let max_validator_count = MaxValidatorsCount::::get().ok_or(Error::::CannotChillOther)?; let current_validator_count = Validators::::count(); ensure!( threshold * max_validator_count < current_validator_count, Error::::CannotChillOther ); Self::min_validator_bond() } else { Zero::zero() }; ensure!(ledger.active < min_active_bond, Error::::CannotChillOther); } Self::chill_stash(&stash); Ok(()) } /// Force a validator to have at least the minimum commission. This will not affect a /// validator who already has a commission greater than or equal to the minimum. Any account /// can call this. #[pezpallet::call_index(24)] #[pezpallet::weight(T::WeightInfo::force_apply_min_commission())] pub fn force_apply_min_commission( origin: OriginFor, validator_stash: T::AccountId, ) -> DispatchResult { ensure_signed(origin)?; let min_commission = MinCommission::::get(); Validators::::try_mutate_exists(validator_stash, |maybe_prefs| { maybe_prefs .as_mut() .map(|prefs| { (prefs.commission < min_commission) .then(|| prefs.commission = min_commission) }) .ok_or(Error::::NotStash) })?; Ok(()) } /// Sets the minimum amount of commission that each validators must maintain. /// /// This call has lower privilege requirements than `set_staking_config` and can be called /// by the `T::AdminOrigin`. Root can always call this. #[pezpallet::call_index(25)] #[pezpallet::weight(T::WeightInfo::set_min_commission())] pub fn set_min_commission(origin: OriginFor, new: Perbill) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; MinCommission::::put(new); Ok(()) } /// Pay out a page of the stakers behind a validator for the given era and page. /// /// - `validator_stash` is the stash account of the validator. /// - `era` may be any era between `[current_era - history_depth; current_era]`. /// - `page` is the page index of nominators to pay out with value between 0 and /// `num_nominators / T::MaxExposurePageSize`. /// /// The origin of this call must be _Signed_. Any account can call this function, even if /// it is not one of the stakers. /// /// If a validator has more than [`Config::MaxExposurePageSize`] nominators backing /// them, then the list of nominators is paged, with each page being capped at /// [`Config::MaxExposurePageSize`.] If a validator has more than one page of nominators, /// the call needs to be made for each page separately in order for all the nominators /// backing a validator to receive the reward. The nominators are not sorted across pages /// and so it should not be assumed the highest staker would be on the topmost page and vice /// versa. If rewards are not claimed in [`Config::HistoryDepth`] eras, they are lost. #[pezpallet::call_index(26)] #[pezpallet::weight(T::WeightInfo::payout_stakers_alive_staked( T::MaxExposurePageSize::get() ))] pub fn payout_stakers_by_page( origin: OriginFor, validator_stash: T::AccountId, era: EraIndex, page: Page, ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; Self::do_payout_stakers_by_page(validator_stash, era, page) } /// Migrates an account's `RewardDestination::Controller` to /// `RewardDestination::Account(controller)`. /// /// Effects will be felt instantly (as soon as this function is completed successfully). /// /// This will waive the transaction fee if the `payee` is successfully migrated. #[pezpallet::call_index(27)] #[pezpallet::weight(T::WeightInfo::update_payee())] pub fn update_payee( origin: OriginFor, controller: T::AccountId, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; let ledger = Self::ledger(StakingAccount::Controller(controller.clone()))?; ensure!( (Payee::::get(&ledger.stash) == { #[allow(deprecated)] Some(RewardDestination::Controller) }), Error::::NotController ); let _ = ledger .set_payee(RewardDestination::Account(controller)) .defensive_proof("ledger should have been previously retrieved from storage.")?; Ok(Pays::No.into()) } /// Updates a batch of controller accounts to their corresponding stash account if they are /// not the same. Ignores any controller accounts that do not exist, and does not operate if /// the stash and controller are already the same. /// /// Effects will be felt instantly (as soon as this function is completed successfully). /// /// The dispatch origin must be `T::AdminOrigin`. #[pezpallet::call_index(28)] #[pezpallet::weight(T::WeightInfo::deprecate_controller_batch(controllers.len() as u32))] pub fn deprecate_controller_batch( origin: OriginFor, controllers: BoundedVec, ) -> DispatchResultWithPostInfo { T::AdminOrigin::ensure_origin(origin)?; // Ignore controllers that do not exist or are already the same as stash. let filtered_batch_with_ledger: Vec<_> = controllers .iter() .filter_map(|controller| { let ledger = Self::ledger(StakingAccount::Controller(controller.clone())); ledger.ok().map_or(None, |ledger| { // If the controller `RewardDestination` is still the deprecated // `Controller` variant, skip deprecating this account. let payee_deprecated = Payee::::get(&ledger.stash) == { #[allow(deprecated)] Some(RewardDestination::Controller) }; if ledger.stash != *controller && !payee_deprecated { Some(ledger) } else { None } }) }) .collect(); // Update unique pairs. let mut failures = 0; for ledger in filtered_batch_with_ledger { let _ = ledger.clone().set_controller_to_stash().map_err(|_| failures += 1); } Self::deposit_event(Event::::ControllerBatchDeprecated { failures }); Ok(Some(T::WeightInfo::deprecate_controller_batch(controllers.len() as u32)).into()) } /// Restores the state of a ledger which is in an inconsistent state. /// /// The requirements to restore a ledger are the following: /// * The stash is bonded; or /// * The stash is not bonded but it has a staking lock left behind; or /// * If the stash has an associated ledger and its state is inconsistent; or /// * If the ledger is not corrupted *but* its staking lock is out of sync. /// /// The `maybe_*` input parameters will overwrite the corresponding data and metadata of the /// ledger associated with the stash. If the input parameters are not set, the ledger will /// be reset values from on-chain state. #[pezpallet::call_index(29)] #[pezpallet::weight(T::WeightInfo::restore_ledger())] pub fn restore_ledger( origin: OriginFor, stash: T::AccountId, maybe_controller: Option, maybe_total: Option>, maybe_unlocking: Option>, T::MaxUnlockingChunks>>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; // cannot restore ledger for virtual stakers. ensure!(!Self::is_virtual_staker(&stash), Error::::VirtualStakerNotAllowed); let current_lock = asset::staked::(&stash); let stash_balance = asset::stakeable_balance::(&stash); let (new_controller, new_total) = match Self::inspect_bond_state(&stash) { Ok(LedgerIntegrityState::Corrupted) => { let new_controller = maybe_controller.unwrap_or(stash.clone()); let new_total = if let Some(total) = maybe_total { let new_total = total.min(stash_balance); // enforce hold == ledger.amount. asset::update_stake::(&stash, new_total)?; new_total } else { current_lock }; Ok((new_controller, new_total)) }, Ok(LedgerIntegrityState::CorruptedKilled) => { if current_lock == Zero::zero() { // this case needs to restore both lock and ledger, so the new total needs // to be given by the called since there's no way to restore the total // on-chain. ensure!(maybe_total.is_some(), Error::::CannotRestoreLedger); Ok(( stash.clone(), maybe_total.expect("total exists as per the check above; qed."), )) } else { Ok((stash.clone(), current_lock)) } }, Ok(LedgerIntegrityState::LockCorrupted) => { // ledger is not corrupted but its locks are out of sync. In this case, we need // to enforce a new ledger.total and staking lock for this stash. let new_total = maybe_total.ok_or(Error::::CannotRestoreLedger)?.min(stash_balance); asset::update_stake::(&stash, new_total)?; Ok((stash.clone(), new_total)) }, Err(Error::::BadState) => { // the stash and ledger do not exist but lock is lingering. asset::kill_stake::(&stash)?; ensure!( Self::inspect_bond_state(&stash) == Err(Error::::NotStash), Error::::BadState ); return Ok(()); }, Ok(LedgerIntegrityState::Ok) | Err(_) => Err(Error::::CannotRestoreLedger), }?; // re-bond stash and controller tuple. Bonded::::insert(&stash, &new_controller); // resoter ledger state. let mut ledger = StakingLedger::::new(stash.clone(), new_total); ledger.controller = Some(new_controller); ledger.unlocking = maybe_unlocking.unwrap_or_default(); ledger.update()?; ensure!( Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok), Error::::BadState ); Ok(()) } /// Migrates permissionlessly a stash from locks to holds. /// /// This removes the old lock on the stake and creates a hold on it atomically. If all /// stake cannot be held, the best effort is made to hold as much as possible. The remaining /// stake is removed from the ledger. /// /// The fee is waived if the migration is successful. #[pezpallet::call_index(30)] #[pezpallet::weight(T::WeightInfo::migrate_currency())] pub fn migrate_currency( origin: OriginFor, stash: T::AccountId, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; Self::do_migrate_currency(&stash)?; // Refund the transaction fee if successful. Ok(Pays::No.into()) } /// Manually and permissionlessly applies a deferred slash for a given era. /// /// Normally, slashes are automatically applied shortly after the start of the `slash_era`. /// The automatic application of slashes is handled by the pezpallet's internal logic, and /// it tries to apply one slash page per block of the era. /// If for some reason, one era is not enough for applying all slash pages, the remaining /// slashes need to be manually (permissionlessly) applied. /// /// For a given era x, if at era x+1, slashes are still unapplied, all withdrawals get /// blocked, and these need to be manually applied by calling this function. /// This function exists as a **fallback mechanism** for this extreme situation, but we /// never expect to encounter this in normal scenarios. /// /// The parameters for this call can be queried by looking at the `UnappliedSlashes` storage /// for eras older than the active era. /// /// ## Parameters /// - `slash_era`: The staking era in which the slash was originally scheduled. /// - `slash_key`: A unique identifier for the slash, represented as a tuple: /// - `stash`: The stash account of the validator being slashed. /// - `slash_fraction`: The fraction of the stake that was slashed. /// - `page_index`: The index of the exposure page being processed. /// /// ## Behavior /// - The function is **permissionless**—anyone can call it. /// - The `slash_era` **must be the current era or a past era**. /// If it is in the future, the /// call fails with `EraNotStarted`. /// - The fee is waived if the slash is successfully applied. /// /// ## Future Improvement /// - Implement an **off-chain worker (OCW) task** to automatically apply slashes when there /// is unused block space, improving efficiency. #[pezpallet::call_index(31)] #[pezpallet::weight(T::WeightInfo::apply_slash())] pub fn apply_slash( origin: OriginFor, slash_era: EraIndex, slash_key: (T::AccountId, Perbill, u32), ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; let active_era = ActiveEra::::get().map(|a| a.index).unwrap_or_default(); ensure!(slash_era <= active_era, Error::::EraNotStarted); // Check if this slash has been cancelled ensure!( !Self::check_slash_cancelled(slash_era, &slash_key.0, slash_key.1), Error::::CancelledSlash ); let unapplied_slash = UnappliedSlashes::::take(&slash_era, &slash_key) .ok_or(Error::::InvalidSlashRecord)?; slashing::apply_slash::(unapplied_slash, slash_era); Ok(Pays::No.into()) } /// Perform one step of era pruning to prevent PoV size exhaustion from unbounded deletions. /// /// This extrinsic enables permissionless lazy pruning of era data by performing /// incremental deletion of storage items. Each call processes a limited number /// of items based on available block weight to avoid exceeding block limits. /// /// Returns `Pays::No` when work is performed to incentivize regular maintenance. /// Anyone can call this to help maintain the chain's storage health. /// /// The era must be eligible for pruning (older than HistoryDepth + 1). /// Check `EraPruningState` storage to see if an era needs pruning before calling. #[pezpallet::call_index(32)] // NOTE: as pre-dispatch weight, use the maximum of all possible pruning step weights #[pezpallet::weight({ let v = T::MaxValidatorSet::get(); T::WeightInfo::prune_era_stakers_paged(v) .max(T::WeightInfo::prune_era_stakers_overview(v)) .max(T::WeightInfo::prune_era_validator_prefs(v)) .max(T::WeightInfo::prune_era_claimed_rewards(v)) .max(T::WeightInfo::prune_era_validator_reward()) .max(T::WeightInfo::prune_era_reward_points()) .max(T::WeightInfo::prune_era_total_stake()) })] pub fn prune_era_step(origin: OriginFor, era: EraIndex) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; // Verify era is eligible for pruning: era <= active_era - history_depth - 1 let active_era = crate::session_rotation::Rotator::::active_era(); let history_depth = T::HistoryDepth::get(); let earliest_prunable_era = active_era.saturating_sub(history_depth).saturating_sub(1); ensure!(era <= earliest_prunable_era, Error::::EraNotPrunable); let actual_weight = Self::do_prune_era_step(era)?; Ok(pezframe_support::dispatch::PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: pezframe_support::dispatch::Pays::No, }) } } }