pallet_staking_async/lib.rs
1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Staking Async Pallet
19//!
20//! This pallet is a fork of the original `pallet-staking`, with a number of key differences:
21//!
22//! * It no longer has access to a secure timestamp, previously used to calculate the duration of an
23//! era.
24//! * It no longer has access to a pallet-session.
25//! * It no longer has access to a pallet-authorship.
26//! * It is capable of working with a multi-page `ElectionProvider``, aka.
27//! `pallet-election-provider-multi-block`.
28//!
29//! While `pallet-staking` was somewhat general-purpose, this pallet is absolutely NOT right from
30//! the get-go: It is designed to be used ONLY in Polkadot/Kusama AssetHub system parachains.
31//!
32//! ## Reward and Inflation
33//!
34//! This pallet supports two reward modes, controlled by [`Config::DisableMinting`]:
35//!
36//! ### Non-minting mode (`DisableMinting = true`)
37//!
38//! Staking does **not** mint tokens. It expects an external source (e.g. `pallet-dap`) to
39//! fund the general staker reward pot ([`PotAccountProvider`]). At each era boundary,
40//! staking snapshots the accumulated balance into an era-specific pot via
41//! [`EraRewardManager`](reward::EraRewardManager). Payouts transfer from the era pot.
42//!
43//! Unclaimed rewards from expired eras (past `HistoryDepth`) are withdrawn and passed to
44//! [`Config::UnclaimedRewardHandler`].
45//!
46//! [`DisableMintingGuard`] is set on the first successful snapshot as a safety net — it
47//! prevents the payout side from falling back to legacy minting for eras that should have
48//! reward pots.
49//!
50//! ### Legacy minting mode (`DisableMinting = false`)
51//!
52//! At era boundary, [`Config::EraPayout`] computes inflation based on `total_staked`,
53//! `total_issuance`, and era duration. Tokens are minted on-the-fly during `payout_stakers`.
54//! The treasury remainder is sent to [`Config::RewardRemainder`]. [`MaxStakedRewards`] can
55//! cap the staker portion. [`Config::MaxEraDuration`] caps the effective era duration.
56//!
57//! This mode is kept for Kusama/non-polkadot runtime's compatibility where inflation depends on the
58//! staking ratio.
59//!
60//! ### Switching modes
61//!
62//! Switching from legacy to non-minting is a one-way migration. Once `DisableMinting` is
63//! set to `true`, it must **never** be switched back — eras created in non-minting mode
64//! have funded reward pots, and switching to legacy would orphan those pots and cause
65//! double-minting.
66//!
67//! ## Slashing Pipeline and Withdrawal Restrictions
68//!
69//! This pallet implements a robust slashing mechanism that ensures the integrity of the staking
70//! system while preventing stakers from withdrawing funds that might still be subject to slashing.
71//!
72//! ### Overview of the Slashing Pipeline
73//!
74//! The slashing process consists of multiple phases:
75//!
76//! 1. **Offence Reporting**: Offences are reported from the relay chain through `on_new_offences`
77//! 2. **Queuing**: Valid offences are added to the `OffenceQueue` for processing
78//! 3. **Processing**: Offences are processed incrementally over multiple blocks
79//! 4. **Application**: Slashes are either applied immediately or deferred based on configuration
80//!
81//! ### Phase 1: Offence Reporting
82//!
83//! Offences are reported from the relay chain (e.g., from BABE, GRANDPA, BEEFY, or parachain
84//! modules) through the `on_new_offences` function:
85//!
86//! ```text
87//! struct Offence {
88//! offender: AccountId, // The validator being slashed
89//! reporters: Vec<AccountId>, // Who reported the offence (may be empty)
90//! slash_fraction: Perbill, // Percentage of stake to slash
91//! }
92//! ```
93//!
94//! **Reporting Deadlines**:
95//! - With deferred slashing: Offences must be reported within `SlashDeferDuration - 1` eras
96//! - With immediate slashing: Offences can be reported up to `BondingDuration` eras old
97//!
98//! Example: If `SlashDeferDuration = 27` and current era is 100:
99//! - Oldest reportable offence: Era 74 (100 - 26)
100//! - Offences from era 73 or earlier are rejected
101//!
102//! ### Phase 2: Queuing
103//!
104//! When an offence passes validation, it's added to the queue:
105//!
106//! 1. **Storage**: Added to `OffenceQueue`: `(EraIndex, AccountId) -> OffenceRecord`
107//! 2. **Era Tracking**: Era added to `OffenceQueueEras` (sorted vector of eras with offences)
108//! 3. **Duplicate Handling**: If an offence already exists for the same validator in the same era,
109//! only the higher slash fraction is kept
110//!
111//! ### Phase 3: Processing
112//!
113//! Offences are processed incrementally in `on_initialize` each block:
114//!
115//! ```text
116//! 1. Load oldest offence from queue
117//! 2. Move to `ProcessingOffence` storage
118//! 3. For each exposure page (from last to first):
119//! - Calculate slash for validator's own stake
120//! - Calculate slash for each nominator (pro-rata based on exposure)
121//! - Track total slash and reward amounts
122//! 4. Once all pages processed, create `UnappliedSlash`
123//! ```
124//!
125//! **Key Features**:
126//! - **Page-by-page processing**: Large validator sets don't overwhelm a single block
127//! - **Pro-rata slashing**: Nominators slashed proportionally to their stake
128//! - **Reward calculation**: A portion goes to reporters (if any)
129//!
130//! ### Phase 4: Application
131//!
132//! Based on `SlashDeferDuration`, slashes are either:
133//!
134//! **Immediate (SlashDeferDuration = 0)**:
135//! - Applied right away in the same block
136//! - Funds deducted from staking ledger immediately
137//!
138//! **Deferred (SlashDeferDuration > 0)**:
139//! - Stored in `UnappliedSlashes` for future application
140//! - Applied at era: `offence_era + SlashDeferDuration`
141//! - Can be cancelled by governance before application
142//!
143//! ### Storage Items Involved
144//!
145//! - `OffenceQueue`: Pending offences to process
146//! - `OffenceQueueEras`: Sorted list of eras with offences
147//! - `ProcessingOffence`: Currently processing offence
148//! - `ValidatorSlashInEra`: Tracks highest slash per validator per era
149//! - `UnappliedSlashes`: Deferred slashes waiting for application
150//!
151//! ### Withdrawal Restrictions
152//!
153//! To maintain slashing guarantees, withdrawals are restricted:
154//!
155//! **Withdrawal Era Calculation**:
156//! ```text
157//! earliest_era_to_withdraw = min(
158//! active_era,
159//! last_fully_processed_offence_era + BondingDuration
160//! )
161//! ```
162//!
163//! **Example**:
164//! - Active era: 100
165//! - Oldest unprocessed offence: Era 70
166//! - BondingDuration: 28
167//! - Withdrawal allowed only for chunks with era ≤ 97 (70 - 1 + 28)
168//!
169//! **Withdrawal Timeline Example with an Offence**:
170//! ```text
171//! Era: 90 91 92 93 94 95 96 97 98 99 100 ... 117 118
172//! | | | | | | | | | | | | |
173//! Unbond: U
174//! Offence: X
175//! Reported: R
176//! Processed: P (within next few blocks)
177//! Slash Applied: S
178//! Withdraw: ❌ ✓
179//!
180//! With BondingDuration = 28 and SlashDeferDuration = 27:
181//! - User unbonds in era 90
182//! - Offence occurs in era 90
183//! - Reported in era 92 (typically within 2 days, but reportable until Era 116)
184//! - Processed in era 92 (within next few blocks after reporting)
185//! - Slash deferred for 27 eras, applied at era 117 (90 + 27)
186//! - Cannot withdraw unbonded chunks until era 118 (90 + 28)
187//!
188//! The 28-era bonding duration ensures that any offences committed before or during
189//! unbonding have time to be reported, processed, and applied before funds can be
190//! withdrawn. This provides a window for governance to cancel slashes that may have
191//! resulted from software bugs.
192//! ```
193//!
194//! **Key Restrictions**:
195//! 1. Cannot withdraw if previous era has unapplied slashes
196//! 2. Cannot withdraw funds from eras with unprocessed offences
197
198#![cfg_attr(not(feature = "std"), no_std)]
199#![recursion_limit = "256"]
200
201#[cfg(feature = "runtime-benchmarks")]
202pub mod benchmarking;
203#[cfg(any(feature = "runtime-benchmarks", test))]
204pub mod testing_utils;
205
206#[cfg(test)]
207pub(crate) mod mock;
208#[cfg(test)]
209mod tests;
210
211pub mod asset;
212pub mod election_size_tracker;
213pub mod ledger;
214pub mod migrations;
215mod pallet;
216pub mod reward;
217pub mod session_rotation;
218pub mod slashing;
219pub mod weights;
220
221extern crate alloc;
222use alloc::{vec, vec::Vec};
223use codec::{Decode, DecodeWithMemTracking, Encode, HasCompact, MaxEncodedLen};
224use frame_election_provider_support::ElectionProvider;
225use frame_support::{
226 traits::{
227 tokens::fungible::{Credit, Debt},
228 ConstU32, Contains, Get, LockIdentifier,
229 },
230 weights::Weight,
231 BoundedVec, DebugNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, WeakBoundedVec,
232};
233use frame_system::pallet_prelude::BlockNumberFor;
234use ledger::LedgerIntegrityState;
235use scale_info::TypeInfo;
236use sp_runtime::{
237 traits::{AtLeast32BitUnsigned, One, StaticLookup, UniqueSaturatedInto},
238 BoundedBTreeMap, Debug, Perbill, Saturating,
239};
240use sp_staking::{EraIndex, ExposurePage, PagedExposureMetadata, SessionIndex};
241pub use sp_staking::{Exposure, IndividualExposure, StakerStatus};
242pub use weights::WeightInfo;
243
244// public exports
245pub use ledger::{StakingLedger, UnlockChunk};
246pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap};
247
248pub(crate) const STAKING_ID: LockIdentifier = *b"staking ";
249pub(crate) const LOG_TARGET: &str = "runtime::staking-async";
250
251// syntactic sugar for logging.
252#[macro_export]
253macro_rules! log {
254 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
255 log::$level!(
256 target: crate::LOG_TARGET,
257 concat!("[{:?}] 💸 ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
258 )
259 };
260}
261
262/// Alias for a bounded set of exposures behind a validator, parameterized by this pallet's
263/// election provider.
264pub type BoundedExposuresOf<T> = BoundedVec<
265 (
266 <T as frame_system::Config>::AccountId,
267 Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
268 ),
269 MaxWinnersPerPageOf<<T as Config>::ElectionProvider>,
270>;
271
272/// Alias for the maximum number of winners (aka. active validators), as defined in by this pallet's
273/// config.
274pub type MaxWinnersOf<T> = <T as Config>::MaxValidatorSet;
275
276/// Alias for the maximum number of winners per page, as expected by the election provider.
277pub type MaxWinnersPerPageOf<P> = <P as ElectionProvider>::MaxWinnersPerPage;
278
279/// Maximum number of nominations per nominator.
280pub type MaxNominationsOf<T> =
281 <<T as Config>::NominationsQuota as NominationsQuota<BalanceOf<T>>>::MaxNominations;
282
283/// Counter for the number of "reward" points earned by a given validator.
284pub type RewardPoint = u32;
285
286/// The balance type of this pallet.
287pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
288
289type PositiveImbalanceOf<T> = Debt<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
290pub type NegativeImbalanceOf<T> =
291 Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
292
293type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
294
295/// Information regarding the active era (era in used in session).
296#[derive(Encode, Decode, Debug, TypeInfo, MaxEncodedLen, PartialEq, Eq, Clone)]
297pub struct ActiveEraInfo {
298 /// Index of era.
299 pub index: EraIndex,
300 /// Moment of start expressed as millisecond from `$UNIX_EPOCH`.
301 ///
302 /// Start can be none if start hasn't been set for the era yet,
303 /// Start is set on the first on_finalize of the era to guarantee usage of `Time`.
304 pub start: Option<u64>,
305}
306
307/// Reward points of an era. Used to split era total payout between validators.
308///
309/// This points will be used to reward validators and their respective nominators.
310#[derive(
311 PartialEqNoBound, Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen, DefaultNoBound,
312)]
313#[codec(mel_bound())]
314#[scale_info(skip_type_params(T))]
315pub struct EraRewardPoints<T: Config> {
316 /// Total number of points. Equals the sum of reward points for each validator.
317 pub total: RewardPoint,
318 /// The reward points earned by a given validator.
319 pub individual: BoundedBTreeMap<T::AccountId, RewardPoint, T::MaxValidatorSet>,
320}
321
322/// A destination account for payment.
323#[derive(
324 PartialEq,
325 Eq,
326 Copy,
327 Clone,
328 Encode,
329 Decode,
330 DecodeWithMemTracking,
331 Debug,
332 TypeInfo,
333 MaxEncodedLen,
334)]
335pub enum RewardDestination<AccountId> {
336 /// Pay into the stash account, increasing the amount at stake accordingly.
337 Staked,
338 /// Pay into the stash account, not increasing the amount at stake.
339 Stash,
340 #[deprecated(
341 note = "`Controller` will be removed after January 2024. Use `Account(controller)` instead."
342 )]
343 Controller,
344 /// Pay into a specified account.
345 Account(AccountId),
346 /// Receive no reward.
347 None,
348}
349
350/// Preference of what happens regarding validation.
351#[derive(
352 PartialEq,
353 Eq,
354 Clone,
355 Encode,
356 Decode,
357 DecodeWithMemTracking,
358 Debug,
359 TypeInfo,
360 Default,
361 MaxEncodedLen,
362)]
363pub struct ValidatorPrefs {
364 /// Reward that validator takes up-front; only the rest is split between themselves and
365 /// nominators.
366 #[codec(compact)]
367 pub commission: Perbill,
368 /// Whether or not this validator is accepting more nominations. If `true`, then no nominator
369 /// who is not already nominating this validator may nominate them. By default, validators
370 /// are accepting nominations.
371 pub blocked: bool,
372}
373
374/// Status of a paged snapshot progress.
375#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Default)]
376pub enum SnapshotStatus<AccountId> {
377 /// Paged snapshot is in progress, the `AccountId` was the last staker iterated in the list.
378 Ongoing(AccountId),
379 /// All the stakers in the system have been consumed since the snapshot started.
380 Consumed,
381 /// Waiting for a new snapshot to be requested.
382 #[default]
383 Waiting,
384}
385
386/// A record of the nominations made by a specific account.
387#[derive(
388 PartialEqNoBound, EqNoBound, Clone, Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen,
389)]
390#[codec(mel_bound())]
391#[scale_info(skip_type_params(T))]
392pub struct Nominations<T: Config> {
393 /// The targets of nomination.
394 pub targets: BoundedVec<T::AccountId, MaxNominationsOf<T>>,
395 /// The era the nominations were submitted.
396 ///
397 /// Except for initial nominations which are considered submitted at era 0.
398 pub submitted_in: EraIndex,
399 /// Whether the nominations have been suppressed. This can happen due to slashing of the
400 /// validators, or other events that might invalidate the nomination.
401 ///
402 /// NOTE: this for future proofing and is thus far not used.
403 pub suppressed: bool,
404}
405
406/// Facade struct to encapsulate `PagedExposureMetadata` and a single page of `ExposurePage`.
407///
408/// This is useful where we need to take into account the validator's own stake and total exposure
409/// in consideration, in addition to the individual nominators backing them.
410#[derive(Encode, Decode, Debug, TypeInfo, PartialEq, Eq)]
411pub struct PagedExposure<AccountId, Balance: HasCompact + codec::MaxEncodedLen> {
412 exposure_metadata: PagedExposureMetadata<Balance>,
413 exposure_page: ExposurePage<AccountId, Balance>,
414}
415
416impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEncodedLen>
417 PagedExposure<AccountId, Balance>
418{
419 /// Create a new instance of `PagedExposure` from legacy clipped exposures.
420 pub fn from_clipped(exposure: Exposure<AccountId, Balance>) -> Self {
421 Self {
422 exposure_metadata: PagedExposureMetadata {
423 total: exposure.total,
424 own: exposure.own,
425 nominator_count: exposure.others.len() as u32,
426 page_count: 1,
427 },
428 exposure_page: ExposurePage { page_total: exposure.total, others: exposure.others },
429 }
430 }
431
432 /// Create a new instance of `PagedExposure` from just the exposure metadata (overview).
433 ///
434 /// This creates a `PagedExposure` with an empty `others` list, useful when only the
435 /// validator's own stake needs to be considered (e.g., when nominators are not slashable).
436 pub fn from_overview(overview: PagedExposureMetadata<Balance>) -> Self {
437 Self {
438 exposure_metadata: PagedExposureMetadata {
439 total: overview.total,
440 own: overview.own,
441 nominator_count: overview.nominator_count,
442 page_count: 1,
443 },
444 exposure_page: ExposurePage { page_total: overview.total, others: vec![] },
445 }
446 }
447
448 /// Returns total exposure of this validator across pages
449 pub fn total(&self) -> Balance {
450 self.exposure_metadata.total
451 }
452
453 /// Returns total exposure of this validator for the current page
454 pub fn page_total(&self) -> Balance {
455 self.exposure_page.page_total + self.exposure_metadata.own
456 }
457
458 /// Returns validator's own stake that is exposed
459 pub fn own(&self) -> Balance {
460 self.exposure_metadata.own
461 }
462
463 /// Returns the portions of nominators stashes that are exposed in this page.
464 pub fn others(&self) -> &Vec<IndividualExposure<AccountId, Balance>> {
465 &self.exposure_page.others
466 }
467}
468
469/// A pending slash record. The value of the slash has been computed but not applied yet,
470/// rather deferred for several eras.
471#[derive(Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen, PartialEqNoBound, EqNoBound)]
472#[scale_info(skip_type_params(T))]
473pub struct UnappliedSlash<T: Config> {
474 /// The stash ID of the offending validator.
475 pub validator: T::AccountId,
476 /// The validator's own slash.
477 pub own: BalanceOf<T>,
478 /// All other slashed stakers and amounts.
479 pub others: WeakBoundedVec<(T::AccountId, BalanceOf<T>), T::MaxExposurePageSize>,
480 /// Reporters of the offence; bounty payout recipients.
481 pub reporter: Option<T::AccountId>,
482 /// The amount of payout.
483 pub payout: BalanceOf<T>,
484}
485
486/// Something that defines the maximum number of nominations per nominator based on a curve.
487///
488/// The method `curve` implements the nomination quota curve and should not be used directly.
489/// However, `get_quota` returns the bounded maximum number of nominations based on `fn curve` and
490/// the nominator's balance.
491pub trait NominationsQuota<Balance> {
492 /// Strict maximum number of nominations that caps the nominations curve. This value can be
493 /// used as the upper bound of the number of votes per nominator.
494 type MaxNominations: Get<u32>;
495
496 /// Returns the voter's nomination quota within reasonable bounds [`min`, `max`], where `min`
497 /// is 1 and `max` is `Self::MaxNominations`.
498 fn get_quota(balance: Balance) -> u32 {
499 Self::curve(balance).clamp(1, Self::MaxNominations::get())
500 }
501
502 /// Returns the voter's nomination quota based on its balance and a curve.
503 fn curve(balance: Balance) -> u32;
504}
505
506/// Check if validator was inactive at some era.
507///
508/// The check is based on the [`RewardPoint`] amount received during the era by the given validator.
509pub trait IsValidatorInactive<AccountId> {
510 /// Tell if the validator considered inactive.
511 fn is_inactive(era: EraIndex, stash: &AccountId, era_points: RewardPoint) -> bool;
512}
513
514impl<AccountId> IsValidatorInactive<AccountId> for () {
515 fn is_inactive(_era: EraIndex, _stash: &AccountId, era_points: RewardPoint) -> bool {
516 era_points == 0
517 }
518}
519
520/// Notified when a new era becomes active.
521pub trait OnEraStart<AccountId> {
522 /// Whether the hook is in use. When `false`, staking does not keep the elected set, and at
523 /// era start it only removes a copy kept while the hook was enabled, charged as one read and
524 /// one write.
525 ///
526 /// Expected to be constant for a given runtime version.
527 fn enabled() -> bool {
528 true
529 }
530
531 /// Called once when `era` becomes active, with the validators elected for it.
532 fn on_era_start(era: EraIndex, validators: &[AccountId]);
533
534 /// Upper bound of the weight of [`Self::on_era_start`] for `validators` validators.
535 fn weight(validators: u32) -> Weight;
536}
537
538impl<AccountId> OnEraStart<AccountId> for () {
539 fn enabled() -> bool {
540 false
541 }
542
543 fn on_era_start(_era: EraIndex, _validators: &[AccountId]) {}
544
545 fn weight(_validators: u32) -> Weight {
546 Weight::zero()
547 }
548}
549
550/// A nomination quota that allows up to MAX nominations for all validators.
551pub struct FixedNominationsQuota<const MAX: u32>;
552impl<Balance, const MAX: u32> NominationsQuota<Balance> for FixedNominationsQuota<MAX> {
553 type MaxNominations = ConstU32<MAX>;
554
555 fn curve(_: Balance) -> u32 {
556 MAX
557 }
558}
559
560pub use sp_staking::EraPayout;
561
562/// Mode of era-forcing.
563#[derive(
564 Copy,
565 Clone,
566 PartialEq,
567 Eq,
568 Encode,
569 Decode,
570 DecodeWithMemTracking,
571 Debug,
572 TypeInfo,
573 MaxEncodedLen,
574 serde::Serialize,
575 serde::Deserialize,
576)]
577pub enum Forcing {
578 /// Not forcing anything - just let whatever happen.
579 NotForcing,
580 /// Force a new era, then reset to `NotForcing` as soon as it is done.
581 /// Note that this will force to trigger an election until a new era is triggered, if the
582 /// election failed, the next session end will trigger a new election again, until success.
583 ForceNew,
584 /// Avoid a new era indefinitely.
585 ForceNone,
586 /// Force a new era at the end of all sessions indefinitely.
587 ForceAlways,
588}
589
590impl Default for Forcing {
591 fn default() -> Self {
592 Forcing::NotForcing
593 }
594}
595
596/// A utility struct that provides a way to check if a given account is a staker.
597///
598/// This struct implements the `Contains` trait, allowing it to determine whether
599/// a particular account is currently staking by checking if the account exists in
600/// the staking ledger.
601///
602/// Intended to be used in [`crate::Config::Filter`].
603pub struct AllStakers<T: Config>(core::marker::PhantomData<T>);
604
605impl<T: Config> Contains<T::AccountId> for AllStakers<T> {
606 /// Checks if the given account ID corresponds to a staker.
607 ///
608 /// # Returns
609 /// - `true` if the account has an entry in the staking ledger (indicating it is staking).
610 /// - `false` otherwise.
611 fn contains(account: &T::AccountId) -> bool {
612 Ledger::<T>::contains_key(account)
613 }
614}
615
616/// Size of the rotating pool of era-specific pot accounts.
617///
618/// Era pots are addressed by `era % POT_POOL_SIZE`, so a pot account is reused
619/// every `POT_POOL_SIZE` eras instead of a fresh account being created per era.
620/// This bounds the total storage footprint contributed by era pot accounts to a
621/// constant rather than growing with chain age.
622///
623/// Must be strictly greater than [`Config::HistoryDepth`] so that a slot is only
624/// reused after its previous era has been pruned and drained. The
625/// [`integrity_test`] enforces this invariant at runtime startup.
626pub(crate) const POT_POOL_SIZE: u32 = 200;
627
628/// Maps an era index to its slot in the rotating pot pool.
629pub(crate) fn pot_slot(era: EraIndex) -> u32 {
630 era % POT_POOL_SIZE
631}
632
633/// Kind of reward managed by staking pots.
634#[derive(
635 Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, codec::DecodeWithMemTracking, TypeInfo,
636)]
637pub enum RewardKind {
638 /// Staker rewards (nominators + validators).
639 #[codec(index = 0)]
640 StakerRewards,
641 /// Pot for validator self-stake incentive.
642 #[codec(index = 1)]
643 ValidatorSelfStake,
644}
645
646/// Identifies a reward pot account.
647#[derive(
648 Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, codec::DecodeWithMemTracking, TypeInfo,
649)]
650pub enum RewardPot {
651 /// General pot: funded by an external source (e.g. pallet-dap).
652 /// At era boundaries, staking snapshots the balance into an era-specific pot.
653 #[codec(index = 0)]
654 General(RewardKind),
655 /// Era-specific pot: snapshotted from the general pot at era boundaries.
656 /// See `POT_POOL_SIZE` for the slot rotation scheme.
657 #[codec(index = 1)]
658 Era(EraIndex, RewardKind),
659}
660
661/// Trait for generating reward pot account IDs.
662pub trait PotAccountProvider<AccountId> {
663 fn pot_account(pot: RewardPot) -> AccountId;
664}
665
666/// Seed-based pot account provider for production use.
667///
668/// Era pots are derived from `(slot, kind)` where `slot = era % POT_POOL_SIZE`,
669/// so a fixed pool of accounts rotates instead of creating one per era.
670pub struct Seed<S>(core::marker::PhantomData<S>);
671
672impl<AccountId, S> PotAccountProvider<AccountId> for Seed<S>
673where
674 AccountId: codec::FullCodec,
675 S: Get<frame_support::PalletId>,
676{
677 fn pot_account(pot: RewardPot) -> AccountId {
678 use sp_runtime::traits::AccountIdConversion;
679 // Era pots are addressed by slot (`era % POT_POOL_SIZE`), not by the
680 // raw era index, so a fixed pool of accounts rotates instead of
681 // growing per era.
682 let normalized = match pot {
683 RewardPot::Era(era, kind) => RewardPot::Era(pot_slot(era), kind),
684 other => other,
685 };
686 S::get().into_sub_account_truncating(normalized)
687 }
688}
689
690/// Sequential pot account provider for testing.
691///
692/// Mirrors the production rotation: era pots collide every `POT_POOL_SIZE`
693/// eras so tests exercise the same pool reuse path.
694#[cfg(feature = "std")]
695pub struct SequentialTest;
696
697#[cfg(feature = "std")]
698impl<AccountId> PotAccountProvider<AccountId> for SequentialTest
699where
700 AccountId: From<u64>,
701{
702 fn pot_account(pot: RewardPot) -> AccountId {
703 match pot {
704 RewardPot::General(RewardKind::StakerRewards) => AccountId::from(200_000u64),
705 RewardPot::General(RewardKind::ValidatorSelfStake) => AccountId::from(200_001u64),
706 RewardPot::Era(era, RewardKind::StakerRewards) => {
707 AccountId::from(100_000 + (pot_slot(era) as u64 * 10))
708 },
709 RewardPot::Era(era, RewardKind::ValidatorSelfStake) => {
710 AccountId::from(100_000 + (pot_slot(era) as u64 * 10) + 1)
711 },
712 }
713 }
714}
715
716/// Budget recipient for staker rewards.
717///
718/// Exposes the general staker reward pot so DAP can drip inflation into it.
719pub struct StakerRewardRecipient<P>(core::marker::PhantomData<P>);
720
721impl<AccountId, P> sp_staking::budget::BudgetRecipient<AccountId> for StakerRewardRecipient<P>
722where
723 P: PotAccountProvider<AccountId>,
724{
725 fn budget_key() -> sp_staking::budget::BudgetKey {
726 sp_staking::budget::BudgetKey::truncate_from(b"staker_rewards".to_vec())
727 }
728
729 fn pot_account() -> AccountId {
730 P::pot_account(RewardPot::General(RewardKind::StakerRewards))
731 }
732}
733
734/// Budget recipient for validator self-stake incentive.
735///
736/// Exposes the general validator incentive pot so DAP can drip inflation into it.
737pub struct ValidatorIncentiveRecipient<P>(core::marker::PhantomData<P>);
738
739impl<AccountId, P> sp_staking::budget::BudgetRecipient<AccountId> for ValidatorIncentiveRecipient<P>
740where
741 P: PotAccountProvider<AccountId>,
742{
743 fn budget_key() -> sp_staking::budget::BudgetKey {
744 sp_staking::budget::BudgetKey::truncate_from(b"validator_incentive".to_vec())
745 }
746
747 fn pot_account() -> AccountId {
748 P::pot_account(RewardPot::General(RewardKind::ValidatorSelfStake))
749 }
750}
751
752/// A smart type to determine the [`Config::PlanningEraOffset`], given:
753///
754/// * Expected relay session duration, `RS`
755/// * Time taking into consideration for XCM sending, `S`
756///
757/// It will use the estimated election duration, the relay session duration, and add one as it knows
758/// the relay chain will want to buffer validators for one session. This is needed because we use
759/// this in our calculation based on the "active era".
760pub struct PlanningEraOffsetOf<T, RS, S>(core::marker::PhantomData<(T, RS, S)>);
761impl<T: Config, RS: Get<BlockNumberFor<T>>, S: Get<BlockNumberFor<T>>> Get<SessionIndex>
762 for PlanningEraOffsetOf<T, RS, S>
763{
764 fn get() -> SessionIndex {
765 let election_duration = <T::ElectionProvider as ElectionProvider>::duration_with_export();
766 let sessions_needed = (election_duration + S::get()) / RS::get();
767 // add one, because we know the RC session pallet wants to buffer for one session, and
768 // another one cause we will receive activation report one session after that.
769 sessions_needed
770 .saturating_add(One::one())
771 .saturating_add(One::one())
772 .unique_saturated_into()
773 }
774}