pallet_dapp_staking/
types.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! # dApp Staking Module Types
20//!
21//! Contains various types, structs & enums used by the dApp staking implementation.
22//! The main purpose of this is to abstract complexity away from the extrinsic call implementation,
23//! and even more importantly to make the code more testable.
24//!
25//! # Overview
26//!
27//! The following is a high level overview of the implemented structs, enums & types.
28//! For details, please refer to the documentation and code of each individual type.
29//!
30//! ## General Protocol Information
31//!
32//! * `EraNumber` - numeric Id of an era.
33//! * `PeriodNumber` - numeric Id of a period.
34//! * `Subperiod` - an enum describing which subperiod is active in the current period.
35//! * `PeriodInfo` - contains information about the ongoing period, like period number, current subperiod and when will the current subperiod end.
36//! * `PeriodEndInfo` - contains information about a finished past period, like the final era of the period, total amount staked & bonus reward pool.
37//! * `ProtocolState` - contains the most general protocol state info: current era number, block when the era ends, ongoing period info, and whether protocol is in maintenance mode.
38//!
39//! ## DApp Information
40//!
41//! * `DAppId` - a compact unique numeric Id of a dApp.
42//! * `DAppInfo` - contains general information about a dApp, like owner and reward beneficiary, Id and state.
43//! * `ContractStakeAmount` - contains information about how much is staked on a particular contract.
44//!
45//! ## Staker Information
46//!
47//! * `UnlockingChunk` - describes some amount undergoing the unlocking process.
48//! * `StakeAmount` - contains information about the staked amount in a particular era, and period.
49//! * `AccountLedger` - keeps track of total locked & staked balance, unlocking chunks and number of stake entries.
50//! * `SingularStakingInfo` - contains information about a particular staker's stake on a specific smart contract. Used to track loyalty.
51//!
52//! ## Era Information
53//!
54//! * `EraInfo` - contains information about the ongoing era, like how much is locked & staked.
55//! * `EraReward` - contains information about a finished era, like reward pools and total staked amount.
56//! * `EraRewardSpan` - a composite of multiple `EraReward` objects, used to describe a range of finished eras.
57//!
58//! ## Tier Information
59//!
60//! * `TierThreshold` - an enum describing tier entry thresholds as percentages of the total issuance.
61//! * `TierParameters` - contains static information about tiers, like init thresholds, reward & slot distribution.
62//! * `TiersConfiguration` - contains dynamic information about tiers, derived from `TierParameters` and onchain data.
63//! * `DAppTier` - a compact struct describing a dApp's tier.
64//! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era.
65//!
66
67use core::ops::Deref;
68use frame_support::{pallet_prelude::*, BoundedBTreeMap, BoundedVec, DefaultNoBound};
69use parity_scale_codec::{Decode, Encode};
70use serde::{Deserialize, Serialize};
71use sp_arithmetic::fixed_point::FixedU128;
72use sp_runtime::{
73    traits::{CheckedAdd, UniqueSaturatedInto, Zero},
74    FixedPointNumber, Perbill, Permill, Saturating,
75};
76pub use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, vec::Vec};
77
78use astar_primitives::{
79    dapp_staking::{DAppId, EraNumber, PeriodNumber, RankedTier, TierSlots as TierSlotsFunc},
80    Balance, BlockNumber,
81};
82
83use crate::pallet::Config;
84
85// Convenience type for `AccountLedger` usage.
86pub type AccountLedgerFor<T> = AccountLedger<<T as Config>::MaxUnlockingChunks>;
87
88// Convenience type for `DAppTierRewards` usage.
89pub type DAppTierRewardsFor<T> =
90    DAppTierRewards<<T as Config>::MaxNumberOfContracts, <T as Config>::NumberOfTiers>;
91
92// Convenience type for `EraRewardSpan` usage.
93pub type EraRewardSpanFor<T> = EraRewardSpan<<T as Config>::EraRewardSpanLength>;
94
95// Convenience type for `DAppInfo` usage.
96pub type DAppInfoFor<T> = DAppInfo<<T as frame_system::Config>::AccountId>;
97
98// Convenience type for `BonusStatusWrapper` usage.
99pub type BonusStatusWrapperFor<T> = BonusStatusWrapper<<T as Config>::MaxBonusSafeMovesPerPeriod>;
100
101/// TODO: remove it once all BonusStatus are updated and the `ActiveBonusUpdateCursor` storage value is cleanup.
102pub type BonusUpdateStateFor<T> =
103    BonusUpdateState<<T as frame_system::Config>::AccountId, <T as Config>::SmartContract>;
104
105pub type BonusUpdateCursorFor<T> = (
106    <T as frame_system::Config>::AccountId,
107    <T as Config>::SmartContract,
108);
109
110pub type BonusUpdateCursor<AccountId, SmartContract> = (AccountId, SmartContract);
111
112#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)]
113pub enum BonusUpdateState<AccountId, SmartContract> {
114    /// No update in progress yet
115    NotInProgress,
116    /// Update in progress for the current cursor
117    InProgress(BonusUpdateCursor<AccountId, SmartContract>),
118    /// All updates have been finished
119    Finished,
120}
121
122impl<AccountId, SmartContract> Default for BonusUpdateState<AccountId, SmartContract> {
123    fn default() -> Self {
124        BonusUpdateState::<AccountId, SmartContract>::NotInProgress
125    }
126}
127
128/// Simple enum representing errors possible when using sparse bounded vector.
129#[derive(Debug, PartialEq, Eq)]
130pub enum AccountLedgerError {
131    /// Old or future era values cannot be added.
132    InvalidEra,
133    /// Bounded storage capacity exceeded.
134    NoCapacity,
135    /// Invalid period specified.
136    InvalidPeriod,
137    /// Stake amount is to large in respect to what's available.
138    UnavailableStakeFunds,
139    /// Unstake amount is to large in respect to what's staked.
140    UnstakeAmountLargerThanStake,
141    /// Nothing to claim.
142    NothingToClaim,
143    /// Attempt to crate the iterator failed due to incorrect data.
144    InvalidIterator,
145}
146
147/// Distinct subperiods in dApp staking protocol.
148#[derive(
149    Encode,
150    Decode,
151    DecodeWithMemTracking,
152    MaxEncodedLen,
153    Clone,
154    Copy,
155    Debug,
156    PartialEq,
157    Eq,
158    TypeInfo,
159)]
160pub enum Subperiod {
161    /// Subperiod during which the focus is on voting. No rewards are earned during this subperiod.
162    Voting,
163    /// Subperiod during which dApps and stakers earn rewards.
164    BuildAndEarn,
165}
166
167impl Subperiod {
168    /// Next subperiod, after `self`.
169    pub fn next(&self) -> Self {
170        match self {
171            Subperiod::Voting => Subperiod::BuildAndEarn,
172            Subperiod::BuildAndEarn => Subperiod::Voting,
173        }
174    }
175}
176
177/// Info about the ongoing period.
178#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
179pub struct PeriodInfo {
180    /// Period number.
181    #[codec(compact)]
182    pub(crate) number: PeriodNumber,
183    /// Subperiod type.
184    pub(crate) subperiod: Subperiod,
185    /// Era in which the new subperiod starts.
186    #[codec(compact)]
187    pub(crate) next_subperiod_start_era: EraNumber,
188}
189
190impl PeriodInfo {
191    /// `true` if the provided era belongs to the next period, `false` otherwise.
192    /// It's only possible to provide this information correctly for the ongoing `BuildAndEarn` subperiod.
193    pub fn is_next_period(&self, era: EraNumber) -> bool {
194        self.subperiod == Subperiod::BuildAndEarn && self.next_subperiod_start_era <= era
195    }
196}
197
198/// Struct with relevant information for a finished period.
199#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
200pub struct PeriodEndInfo {
201    /// Bonus reward pool allocated for eligible stakers with a non-null bonus status
202    #[codec(compact)]
203    pub(crate) bonus_reward_pool: Balance,
204    /// Total amount staked (remaining) from the voting subperiod.
205    #[codec(compact)]
206    pub(crate) total_vp_stake: Balance,
207    /// Final era, inclusive, in which the period ended.
208    #[codec(compact)]
209    pub(crate) final_era: EraNumber,
210}
211
212/// Force types to speed up the next era, and even period.
213#[derive(
214    Encode,
215    Decode,
216    DecodeWithMemTracking,
217    MaxEncodedLen,
218    Clone,
219    Copy,
220    Debug,
221    PartialEq,
222    Eq,
223    TypeInfo,
224)]
225pub enum ForcingType {
226    /// Force the next era to start.
227    Era,
228    /// Force the current subperiod to end, and new one to start. It will also force a new era to start.
229    Subperiod,
230}
231
232/// General information & state of the dApp staking protocol.
233#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
234pub struct ProtocolState {
235    /// Ongoing era number.
236    #[codec(compact)]
237    pub(crate) era: EraNumber,
238    /// Block number at which the next era should start.
239    #[codec(compact)]
240    pub(crate) next_era_start: BlockNumber,
241    /// Information about the ongoing period.
242    pub(crate) period_info: PeriodInfo,
243    /// `true` if pallet is in maintenance mode (disabled), `false` otherwise.
244    pub(crate) maintenance: bool,
245}
246
247impl Default for ProtocolState {
248    fn default() -> Self {
249        Self {
250            era: 1,
251            next_era_start: 2,
252            period_info: PeriodInfo {
253                number: 1,
254                subperiod: Subperiod::Voting,
255                next_subperiod_start_era: 2,
256            },
257            maintenance: false,
258        }
259    }
260}
261
262impl ProtocolState {
263    /// Ongoing era.
264    pub fn era(&self) -> EraNumber {
265        self.era
266    }
267
268    /// Block number at which the next era should start.
269    pub fn next_era_start(&self) -> BlockNumber {
270        self.next_era_start
271    }
272
273    /// Set the next era start block number.
274    /// Not perfectly clean approach but helps speed up integration tests significantly.
275    pub fn set_next_era_start(&mut self, next_era_start: BlockNumber) {
276        self.next_era_start = next_era_start;
277    }
278
279    /// Current subperiod.
280    pub fn subperiod(&self) -> Subperiod {
281        self.period_info.subperiod
282    }
283
284    /// Current period number.
285    pub fn period_number(&self) -> PeriodNumber {
286        self.period_info.number
287    }
288
289    /// Ending era of current period
290    pub fn next_subperiod_start_era(&self) -> EraNumber {
291        self.period_info.next_subperiod_start_era
292    }
293
294    /// Checks whether a new era should be triggered, based on the provided _current_ block number argument
295    /// or possibly other protocol state parameters.
296    pub fn is_new_era(&self, now: BlockNumber) -> bool {
297        self.next_era_start <= now
298    }
299
300    /// Triggers the next subperiod, updating appropriate parameters.
301    pub fn advance_to_next_subperiod(
302        &mut self,
303        next_subperiod_start_era: EraNumber,
304        next_era_start: BlockNumber,
305    ) {
306        let period_number = match self.subperiod() {
307            Subperiod::Voting => self.period_number(),
308            Subperiod::BuildAndEarn => self.period_number().saturating_add(1),
309        };
310
311        self.period_info = PeriodInfo {
312            number: period_number,
313            subperiod: self.subperiod().next(),
314            next_subperiod_start_era,
315        };
316        self.next_era_start = next_era_start;
317    }
318}
319
320/// General information about a dApp.
321#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
322pub struct DAppInfo<AccountId> {
323    /// Owner of the dApp, default reward beneficiary.
324    pub(crate) owner: AccountId,
325    /// dApp's unique identifier in dApp staking.
326    #[codec(compact)]
327    pub(crate) id: DAppId,
328    // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`.
329    pub(crate) reward_beneficiary: Option<AccountId>,
330}
331
332impl<AccountId> DAppInfo<AccountId> {
333    /// dApp's unique identifier.
334    pub fn id(&self) -> DAppId {
335        self.id
336    }
337
338    /// Reward destination account for this dApp.
339    pub fn reward_beneficiary(&self) -> &AccountId {
340        match &self.reward_beneficiary {
341            Some(account_id) => account_id,
342            None => &self.owner,
343        }
344    }
345}
346
347/// How much was unlocked in some block.
348#[derive(Encode, Decode, MaxEncodedLen, Clone, Default, Copy, Debug, PartialEq, Eq, TypeInfo)]
349pub struct UnlockingChunk {
350    /// Amount undergoing the unlocking period.
351    #[codec(compact)]
352    pub amount: Balance,
353    /// Block in which the unlocking period is finished for this chunk.
354    #[codec(compact)]
355    pub unlock_block: BlockNumber,
356}
357
358/// General info about an account's lock & stakes.
359///
360/// ## Overview
361///
362/// The most complex part about this type are the `staked` and `staked_future` fields.
363/// To understand why the two fields exist and how they are used, it's important to consider some facts:
364/// * when an account _stakes_, the staked amount is only eligible for rewards from the next era
365/// * all stakes are reset when a period ends - but this is done in a lazy fashion, account ledgers aren't directly updated
366/// * `stake` and `unstake` operations are allowed only if the account has claimed all pending rewards
367///
368/// In order to keep track of current era stake, and _next era_ stake, two fields are needed.
369/// Since it's not allowed to stake/unstake if there are pending rewards, it's guaranteed that the `staked` and `staked_future` eras are **always consecutive**.
370/// In order to understand if _stake_ is still valid, it's enough to check the `period` field of either `staked` or `staked_future`.
371///
372/// ## Example
373///
374/// ### Scenario 1
375///
376/// * current era is **20**, and current period is **1**
377/// * `staked` is equal to: `{ voting: 100, build_and_earn: 50, era: 5, period: 1 }`
378/// * `staked_future` is equal to: `{ voting: 100, build_and_earn: 100, era: 6, period: 1 }`
379///
380/// The correct way to interpret this is:
381/// * account had staked **150** in total in era 5
382/// * account had increased their stake to **200** in total in era 6
383/// * since then, era 6, account hadn't staked or unstaked anything or hasn't claimed any rewards
384/// * since we're in era **20** and period is still **1**, the account's stake for eras **7** to **20** is still **200**
385///
386/// ### Scenario 2
387///
388/// * current era is **20**, and current period is **1**
389/// * `staked` is equal to: `{ voting: 0, build_and_earn: 0, era: 0, period: 0 }`
390/// * `staked_future` is equal to: `{ voting: 0, build_and_earn: 350, era: 13, period: 1 }`
391///
392/// The correct way to interpret this is:
393/// * `staked` entry is _empty_
394/// * account had called `stake` during era 12, and staked **350** for the next era
395/// * account hadn't staked, unstaked or claimed rewards since then
396/// * since we're in era **20** and period is still **1**, the account's stake for eras **13** to **20** is still **350**
397///
398/// ### Scenario 3
399///
400/// * current era is **30**, and current period is **2**
401/// * period **1** ended after era **24**, and period **2** started in era **25**
402/// * `staked` is equal to: `{ voting: 100, build_and_earn: 300, era: 20, period: 1 }`
403/// * `staked_future` is equal to `None`
404///
405/// The correct way to interpret this is:
406/// * in era **20**, account had claimed rewards for the past eras, so only the `staked` entry remained
407/// * since then, account hadn't staked, unstaked or claimed rewards
408/// * period 1 ended in era **24**, which means that after that era, the `staked` entry is no longer valid
409/// * account had staked **400** in total from era **20** up to era **24** (inclusive)
410/// * account's stake in era **25** is **zero**
411///
412#[derive(
413    Encode,
414    Decode,
415    MaxEncodedLen,
416    RuntimeDebugNoBound,
417    PartialEqNoBound,
418    DefaultNoBound,
419    EqNoBound,
420    CloneNoBound,
421    TypeInfo,
422)]
423#[scale_info(skip_type_params(UnlockingLen))]
424pub struct AccountLedger<UnlockingLen: Get<u32>> {
425    /// How much active locked amount an account has. This can be used for staking.
426    #[codec(compact)]
427    pub(crate) locked: Balance,
428    /// Vector of all the unlocking chunks. This is also considered _locked_ but cannot be used for staking.
429    pub(crate) unlocking: BoundedVec<UnlockingChunk, UnlockingLen>,
430    /// Primary field used to store how much was staked in a particular era.
431    pub(crate) staked: StakeAmount,
432    /// Secondary field used to store 'stake' information for the 'next era'.
433    /// This is needed since stake amount is only applicable from the next era after it's been staked.
434    ///
435    /// Both `stake` and `staked_future` must ALWAYS refer to the same period.
436    /// If `staked_future` is `Some`, it will always be **EXACTLY** one era after the `staked` field era.
437    pub(crate) staked_future: Option<StakeAmount>,
438    /// Number of contract stake entries in storage.
439    #[codec(compact)]
440    pub(crate) contract_stake_count: u32,
441}
442
443impl<UnlockingLen> AccountLedger<UnlockingLen>
444where
445    UnlockingLen: Get<u32>,
446{
447    /// How much active locked amount an account has. This can be used for staking.
448    pub fn locked(&self) -> Balance {
449        self.locked
450    }
451
452    /// Unlocking chunks.
453    pub fn unlocking_chunks(&self) -> &[UnlockingChunk] {
454        &self.unlocking
455    }
456
457    /// Empty if no locked/unlocking/staked info exists.
458    pub fn is_empty(&self) -> bool {
459        self.locked.is_zero()
460            && self.unlocking.is_empty()
461            && self.staked.total().is_zero()
462            && self.staked_future.is_none()
463    }
464
465    /// Returns active locked amount.
466    /// If `zero`, means that associated account hasn't got any active locked funds.
467    ///
468    /// It is possible that some funds are undergoing the unlocking period, but they aren't considered active in that case.
469    pub fn active_locked_amount(&self) -> Balance {
470        self.locked
471    }
472
473    /// Returns unlocking amount.
474    /// If `zero`, means that associated account hasn't got any unlocking chunks.
475    pub fn unlocking_amount(&self) -> Balance {
476        self.unlocking.iter().fold(Balance::zero(), |sum, chunk| {
477            sum.saturating_add(chunk.amount)
478        })
479    }
480
481    /// Total locked amount by the user.
482    /// Includes both active locked amount & unlocking amount.
483    pub fn total_locked_amount(&self) -> Balance {
484        self.active_locked_amount()
485            .saturating_add(self.unlocking_amount())
486    }
487
488    /// Adds the specified amount to the total locked amount.
489    pub fn add_lock_amount(&mut self, amount: Balance) {
490        self.locked.saturating_accrue(amount);
491    }
492
493    /// Subtracts the specified amount of the total locked amount.
494    pub fn subtract_lock_amount(&mut self, amount: Balance) {
495        self.locked.saturating_reduce(amount);
496    }
497
498    /// Adds the specified amount to the unlocking chunks.
499    ///
500    /// If entry for the specified block already exists, it's updated.
501    ///
502    /// If entry for the specified block doesn't exist, it's created and insertion is attempted.
503    /// In case vector has no more capacity, error is returned, and whole operation is a noop.
504    pub fn add_unlocking_chunk(
505        &mut self,
506        amount: Balance,
507        unlock_block: BlockNumber,
508    ) -> Result<(), AccountLedgerError> {
509        if amount.is_zero() {
510            return Ok(());
511        }
512
513        let idx = self
514            .unlocking
515            .binary_search_by(|chunk| chunk.unlock_block.cmp(&unlock_block));
516
517        match idx {
518            Ok(idx) => {
519                self.unlocking[idx].amount.saturating_accrue(amount);
520            }
521            Err(idx) => {
522                let new_unlocking_chunk = UnlockingChunk {
523                    amount,
524                    unlock_block,
525                };
526                self.unlocking
527                    .try_insert(idx, new_unlocking_chunk)
528                    .map_err(|_| AccountLedgerError::NoCapacity)?;
529            }
530        }
531
532        Ok(())
533    }
534
535    /// Amount available for unlocking.
536    pub fn unlockable_amount(&self, current_period: PeriodNumber) -> Balance {
537        self.active_locked_amount()
538            .saturating_sub(self.staked_amount(current_period))
539    }
540
541    /// Claims all of the fully unlocked chunks, and returns the total claimable amount.
542    pub fn claim_unlocked(&mut self, current_block_number: BlockNumber) -> Balance {
543        let mut total = Balance::zero();
544
545        self.unlocking.retain(|chunk| {
546            if chunk.unlock_block <= current_block_number {
547                total.saturating_accrue(chunk.amount);
548                false
549            } else {
550                true
551            }
552        });
553
554        total
555    }
556
557    /// Consumes all of the unlocking chunks, and returns the total amount being unlocked.
558    pub fn consume_unlocking_chunks(&mut self) -> Balance {
559        let amount = self.unlocking.iter().fold(Balance::zero(), |sum, chunk| {
560            sum.saturating_add(chunk.amount)
561        });
562        self.unlocking = Default::default();
563
564        amount
565    }
566
567    /// Amount that is available for staking.
568    ///
569    /// This is equal to the total active locked amount, minus the staked amount already active.
570    pub fn stakeable_amount(&self, active_period: PeriodNumber) -> Balance {
571        self.active_locked_amount()
572            .saturating_sub(self.staked_amount(active_period))
573    }
574
575    /// Amount that is staked, in respect to the currently active period.
576    pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance {
577        // First check the 'future' entry, afterwards check the 'first' entry
578        match self.staked_future {
579            Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(),
580            _ => match self.staked {
581                stake_amount if stake_amount.period == active_period => stake_amount.total(),
582                _ => Balance::zero(),
583            },
584        }
585    }
586
587    /// How much is staked for the specified subperiod, in respect to the specified era.
588    pub fn staked_amount_for_type(&self, subperiod: Subperiod, period: PeriodNumber) -> Balance {
589        // First check the 'future' entry, afterwards check the 'first' entry
590        match self.staked_future {
591            Some(stake_amount) if stake_amount.period == period => stake_amount.for_type(subperiod),
592            _ => match self.staked {
593                stake_amount if stake_amount.period == period => stake_amount.for_type(subperiod),
594                _ => Balance::zero(),
595            },
596        }
597    }
598
599    /// Check for stake/unstake operation era & period arguments.
600    ///
601    /// Ensures that the provided era & period are valid according to the current ledger state.
602    fn stake_unstake_argument_check(
603        &self,
604        current_era: EraNumber,
605        current_period_info: &PeriodInfo,
606    ) -> Result<(), AccountLedgerError> {
607        if !self.staked.is_empty() {
608            // In case entry for the current era exists, it must match the era exactly.
609            // No other scenario is possible since stake/unstake is not allowed without claiming rewards first.
610            if self.staked.era != current_era {
611                return Err(AccountLedgerError::InvalidEra);
612            }
613            if self.staked.period != current_period_info.number {
614                return Err(AccountLedgerError::InvalidPeriod);
615            }
616            // In case only the 'future' entry exists, then the future era must either be the current or the next era.
617            // 'Next era' covers the simple scenario where stake is only valid from the next era.
618            // 'Current era' covers the scenario where stake was made in previous era, and we've moved to the next era.
619        } else if let Some(stake_amount) = self.staked_future {
620            if stake_amount.era != current_era.saturating_add(1) && stake_amount.era != current_era
621            {
622                return Err(AccountLedgerError::InvalidEra);
623            }
624            if stake_amount.period != current_period_info.number {
625                return Err(AccountLedgerError::InvalidPeriod);
626            }
627        }
628        Ok(())
629    }
630
631    /// Adds the specified amount to total staked amount, if possible.
632    ///
633    /// Staking can only be done for the ongoing period, and era.
634    /// 1. The `period` requirement enforces staking in the ongoing period.
635    /// 2. The `era` requirement enforces staking in the ongoing era.
636    ///
637    /// The 2nd condition is needed to prevent stakers from building a significant history of stakes,
638    /// without claiming the rewards. So if a historic era exists as an entry, stakers will first need to claim
639    /// the pending rewards, before they can stake again.
640    ///
641    /// Additionally, the staked amount must not exceed what's available for staking.
642    pub fn add_stake_amount(
643        &mut self,
644        amount: StakeAmount,
645        current_era: EraNumber,
646        current_period_info: PeriodInfo,
647    ) -> Result<(), AccountLedgerError> {
648        if amount.total().is_zero() {
649            return Ok(());
650        }
651
652        self.stake_unstake_argument_check(current_era, &current_period_info)?;
653
654        if self.stakeable_amount(current_period_info.number) < amount.total() {
655            return Err(AccountLedgerError::UnavailableStakeFunds);
656        }
657
658        // Update existing entry if it exists, otherwise create it.
659        match self.staked_future.as_mut() {
660            Some(stake_amount) => {
661                // In case future entry exists, check if it should be moved over to the 'current' entry.
662                if stake_amount.era == current_era {
663                    self.staked = *stake_amount;
664                }
665
666                stake_amount.add(amount.voting, Subperiod::Voting);
667                stake_amount.add(amount.build_and_earn, Subperiod::BuildAndEarn);
668                stake_amount.era = current_era.saturating_add(1);
669            }
670            None => {
671                let mut stake_amount = self.staked;
672                stake_amount.era = current_era.saturating_add(1);
673                stake_amount.period = current_period_info.number;
674                stake_amount.add(amount.voting, Subperiod::Voting);
675                stake_amount.add(amount.build_and_earn, Subperiod::BuildAndEarn);
676                self.staked_future = Some(stake_amount);
677            }
678        }
679
680        Ok(())
681    }
682
683    /// Subtracts the specified amount from the total staked amount, if possible.
684    ///
685    /// Unstake can only be called if the entry for the current era exists.
686    /// In case historic entry exists, rewards first need to be claimed, before unstaking is possible.
687    /// Similar as with stake functionality, this is to prevent staker from building a significant history of stakes.
688    pub fn unstake_amount(
689        &mut self,
690        amount: Balance,
691        current_era: EraNumber,
692        current_period_info: PeriodInfo,
693    ) -> Result<(), AccountLedgerError> {
694        if amount.is_zero() {
695            return Ok(());
696        }
697
698        self.stake_unstake_argument_check(current_era, &current_period_info)?;
699
700        // User must be precise with their unstake amount.
701        if self.staked_amount(current_period_info.number) < amount {
702            return Err(AccountLedgerError::UnstakeAmountLargerThanStake);
703        }
704
705        self.staked.subtract(amount);
706
707        // Convenience cleanup
708        if self.staked.is_empty() {
709            self.staked = Default::default();
710        }
711
712        if let Some(mut stake_amount) = self.staked_future {
713            stake_amount.subtract(amount);
714
715            self.staked_future = if stake_amount.is_empty() {
716                None
717            } else {
718                Some(stake_amount)
719            };
720        }
721
722        Ok(())
723    }
724
725    /// Period for which account has staking information or `None` if no staking information exists.
726    pub fn staked_period(&self) -> Option<PeriodNumber> {
727        if self.staked.is_empty() {
728            self.staked_future.map(|stake_amount| stake_amount.period)
729        } else {
730            Some(self.staked.period)
731        }
732    }
733
734    /// Earliest era for which the account has staking information or `None` if no staking information exists.
735    pub fn earliest_staked_era(&self) -> Option<EraNumber> {
736        if self.staked.is_empty() {
737            self.staked_future.map(|stake_amount| stake_amount.era)
738        } else {
739            Some(self.staked.era)
740        }
741    }
742
743    /// Cleanup staking information if it has expired.
744    ///
745    /// # Args
746    /// `valid_threshold_period` - last period for which entries can still be considered valid.
747    ///
748    /// `true` if any change was made, `false` otherwise.
749    pub fn maybe_cleanup_expired(&mut self, valid_threshold_period: PeriodNumber) -> bool {
750        match self.staked_period() {
751            Some(staked_period) if staked_period < valid_threshold_period => {
752                self.staked = Default::default();
753                self.staked_future = None;
754                true
755            }
756            _ => false,
757        }
758    }
759
760    /// 'Claim' rewards up to the specified era.
761    /// Returns an iterator over the `(era, amount)` pairs, where `amount`
762    /// describes the staked amount eligible for reward in the appropriate era.
763    ///
764    /// If `period_end` is provided, it's used to determine whether all applicable chunks have been claimed.
765    pub fn claim_up_to_era(
766        &mut self,
767        era: EraNumber,
768        period_end: Option<EraNumber>,
769    ) -> Result<EraStakePairIter, AccountLedgerError> {
770        // Main entry exists, but era isn't 'in history'
771        if !self.staked.is_empty() {
772            ensure!(era >= self.staked.era, AccountLedgerError::NothingToClaim);
773        } else if let Some(stake_amount) = self.staked_future {
774            // Future entry exists, but era isn't 'in history'
775            ensure!(era >= stake_amount.era, AccountLedgerError::NothingToClaim);
776        }
777
778        // There are multiple options:
779        // 1. We only have future entry, no current entry
780        // 2. We have both current and future entry, but are only claiming 1 era
781        // 3. We have both current and future entry, and are claiming multiple eras
782        // 4. We only have current entry, no future entry
783        let (span, maybe_first) = if let Some(stake_amount) = self.staked_future {
784            if self.staked.is_empty() {
785                ((stake_amount.era, era, stake_amount.total()), None)
786            } else if self.staked.era == era {
787                ((era, era, self.staked.total()), None)
788            } else {
789                (
790                    (stake_amount.era, era, stake_amount.total()),
791                    Some((self.staked.era, self.staked.total())),
792                )
793            }
794        } else {
795            ((self.staked.era, era, self.staked.total()), None)
796        };
797
798        let result = EraStakePairIter::new(span, maybe_first)
799            .map_err(|_| AccountLedgerError::InvalidIterator)?;
800
801        // Rollover future to 'current' stake amount
802        if let Some(stake_amount) = self.staked_future.take() {
803            self.staked = stake_amount;
804        }
805        self.staked.era = era.saturating_add(1);
806
807        // Make sure to clean up the entries if all rewards for the period have been claimed.
808        match period_end {
809            Some(period_end_era) if era >= period_end_era => {
810                self.staked = Default::default();
811                self.staked_future = None;
812            }
813            _ => (),
814        }
815
816        Ok(result)
817    }
818}
819
820/// Helper internal struct for iterating over `(era, stake amount)` pairs.
821///
822/// Due to how `AccountLedger` is implemented, few scenarios are possible when claiming rewards:
823///
824/// 1. `staked` has some amount, `staked_future` is `None`
825///   * `maybe_first` is `None`, span describes the entire range
826/// 2. `staked` has nothing, `staked_future` is some and has some amount
827///   * `maybe_first` is `None`, span describes the entire range
828/// 3. `staked` has some amount, `staked_future` has some amount
829///   * `maybe_first` is `Some` and covers the `staked` entry, span describes the entire range except the first pair.
830#[derive(Copy, Clone, Debug, PartialEq, Eq)]
831pub struct EraStakePairIter {
832    /// Denotes whether the first entry is different than the others.
833    maybe_first: Option<(EraNumber, Balance)>,
834    /// Starting era of the span.
835    start_era: EraNumber,
836    /// Ending era of the span, inclusive.
837    end_era: EraNumber,
838    /// Staked amount in the span.
839    amount: Balance,
840}
841
842impl EraStakePairIter {
843    /// Create new iterator struct for `(era, staked amount)` pairs.
844    pub fn new(
845        span: (EraNumber, EraNumber, Balance),
846        maybe_first: Option<(EraNumber, Balance)>,
847    ) -> Result<Self, ()> {
848        // First era must be smaller or equal to the last era.
849        if span.0 > span.1 {
850            return Err(());
851        }
852        // If 'maybe_first' is defined, it must exactly match the `span.0 - 1` era value.
853        match maybe_first {
854            Some((era, _)) if span.0.saturating_sub(era) != 1 => {
855                return Err(());
856            }
857            _ => (),
858        }
859
860        Ok(Self {
861            maybe_first,
862            start_era: span.0,
863            end_era: span.1,
864            amount: span.2,
865        })
866    }
867}
868
869impl Iterator for EraStakePairIter {
870    type Item = (EraNumber, Balance);
871
872    fn next(&mut self) -> Option<Self::Item> {
873        // Fist cover the scenario where we have a unique first value
874        if let Some((era, amount)) = self.maybe_first.take() {
875            return Some((era, amount));
876        }
877
878        // Afterwards, just keep returning the same amount for different eras
879        if self.start_era <= self.end_era {
880            let value = (self.start_era, self.amount);
881            self.start_era.saturating_inc();
882            return Some(value);
883        } else {
884            None
885        }
886    }
887}
888
889/// Describes stake amount in an particular era/period.
890#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
891pub struct StakeAmount {
892    /// Amount of staked funds accounting for the voting subperiod.
893    #[codec(compact)]
894    pub(crate) voting: Balance,
895    /// Amount of staked funds accounting for the build&earn subperiod.
896    #[codec(compact)]
897    pub(crate) build_and_earn: Balance,
898    /// Era to which this stake amount refers to.
899    #[codec(compact)]
900    pub(crate) era: EraNumber,
901    /// Period to which this stake amount refers to.
902    #[codec(compact)]
903    pub(crate) period: PeriodNumber,
904}
905
906impl StakeAmount {
907    /// `true` if nothing is staked, `false` otherwise
908    pub fn is_empty(&self) -> bool {
909        self.voting.is_zero() && self.build_and_earn.is_zero()
910    }
911
912    /// Total amount staked in both subperiods.
913    pub fn total(&self) -> Balance {
914        self.voting.saturating_add(self.build_and_earn)
915    }
916
917    /// Amount staked for the specified subperiod.
918    pub fn for_type(&self, subperiod: Subperiod) -> Balance {
919        match subperiod {
920            Subperiod::Voting => self.voting,
921            Subperiod::BuildAndEarn => self.build_and_earn,
922        }
923    }
924
925    /// Stake the specified `amount` for the specified `subperiod`.
926    pub fn add(&mut self, amount: Balance, subperiod: Subperiod) {
927        match subperiod {
928            Subperiod::Voting => self.voting.saturating_accrue(amount),
929            Subperiod::BuildAndEarn => self.build_and_earn.saturating_accrue(amount),
930        }
931    }
932
933    /// Subtract the specified [`StakeAmount`], updating both `subperiods`.
934    pub fn subtract_stake(&mut self, amount: &StakeAmount) {
935        self.voting.saturating_reduce(amount.voting);
936        self.build_and_earn.saturating_reduce(amount.build_and_earn);
937    }
938
939    /// Unstake the specified `amount`.
940    ///
941    /// Attempt to subtract from `Build&Earn` subperiod amount is done first. Any rollover is subtracted from
942    /// the `Voting` subperiod amount.
943    pub fn subtract(&mut self, amount: Balance) {
944        if self.build_and_earn >= amount {
945            self.build_and_earn.saturating_reduce(amount);
946        } else {
947            // Rollover from build&earn to voting, is guaranteed to be larger than zero due to previous check
948            // E.g. voting = 10, build&earn = 5, amount = 7
949            // underflow = build&earn - amount = 5 - 7 = -2
950            // voting = 10 - 2 = 8
951            // build&earn = 0
952            let remainder = amount.saturating_sub(self.build_and_earn);
953            self.build_and_earn = Balance::zero();
954            self.voting.saturating_reduce(remainder);
955        }
956    }
957
958    /// Returns a new `StakeAmount` representing the difference between `self` and `other`,
959    /// without modifying era or period.
960    pub fn saturating_difference(&self, other: &StakeAmount) -> StakeAmount {
961        StakeAmount {
962            voting: self.voting.saturating_sub(other.voting),
963            build_and_earn: self.build_and_earn.saturating_sub(other.build_and_earn),
964            ..*self // Keep the original `era` and `period`
965        }
966    }
967
968    /// Converts all `Voting` stake into `BuildAndEarn`, effectively forfeiting bonus eligibility.
969    ///
970    /// This is used when a user loses bonus eligibility, ensuring that previously staked
971    /// voting amounts are not lost or mixed with destination 'voting amount' during a move
972    /// operation, but instead reallocated to `BuildAndEarn`.
973    pub fn convert_bonus_into_regular_stake(&mut self) {
974        let forfeited_bonus = self.voting;
975        self.voting = Balance::zero();
976        self.build_and_earn.saturating_accrue(forfeited_bonus);
977    }
978}
979
980/// Info about an era, including the rewards, how much is locked, unlocking, etc.
981#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
982pub struct EraInfo {
983    /// How much balance is locked in dApp staking.
984    /// Does not include the amount that is undergoing the unlocking period.
985    #[codec(compact)]
986    pub(crate) total_locked: Balance,
987    /// How much balance is undergoing unlocking process.
988    /// This amount still counts into locked amount.
989    #[codec(compact)]
990    pub(crate) unlocking: Balance,
991    /// Stake amount valid for the ongoing era.
992    pub(crate) current_stake_amount: StakeAmount,
993    /// Stake amount valid from the next era.
994    pub(crate) next_stake_amount: StakeAmount,
995}
996
997impl EraInfo {
998    /// Stake amount valid for the ongoing era.
999    pub fn current_stake_amount(&self) -> StakeAmount {
1000        self.current_stake_amount
1001    }
1002
1003    /// Stake amount valid from the next era.
1004    pub fn next_stake_amount(&self) -> StakeAmount {
1005        self.next_stake_amount
1006    }
1007
1008    /// Update with the new amount that has just been locked.
1009    pub fn add_locked(&mut self, amount: Balance) {
1010        self.total_locked.saturating_accrue(amount);
1011    }
1012
1013    /// Update with the new amount that has just started undergoing the unlocking period.
1014    pub fn unlocking_started(&mut self, amount: Balance) {
1015        self.total_locked.saturating_reduce(amount);
1016        self.unlocking.saturating_accrue(amount);
1017    }
1018
1019    /// Update with the new amount that has been removed from unlocking.
1020    pub fn unlocking_removed(&mut self, amount: Balance) {
1021        self.unlocking.saturating_reduce(amount);
1022    }
1023
1024    /// Add the specified `amount` to the appropriate stake amount, based on the `Subperiod`.
1025    pub fn add_stake_amount(&mut self, amount: StakeAmount) {
1026        self.next_stake_amount.add(amount.voting, Subperiod::Voting);
1027        self.next_stake_amount
1028            .add(amount.build_and_earn, Subperiod::BuildAndEarn);
1029    }
1030
1031    /// Unstakes the specified amounts by subtracting them from the appropriate stake subperiods.
1032    ///
1033    /// - If an entry belongs to the `current_era`, it reduces `current_stake_amount`.
1034    /// - If an entry belongs to the `next_era`, it reduces `next_stake_amount`.
1035    /// - If the entry is from a past era or invalid, it is ignored.
1036    pub fn unstake_amount(&mut self, stake_amount_entries: impl IntoIterator<Item = StakeAmount>) {
1037        for entry in stake_amount_entries {
1038            if entry.era == self.current_stake_amount.era {
1039                self.current_stake_amount.subtract_stake(&entry);
1040            } else if entry.era == self.next_stake_amount.era {
1041                self.next_stake_amount.subtract_stake(&entry);
1042            }
1043        }
1044    }
1045
1046    /// Total staked amount in this era.
1047    pub fn total_staked_amount(&self) -> Balance {
1048        self.current_stake_amount.total()
1049    }
1050
1051    /// Staked amount of specified `type` in this era.
1052    pub fn staked_amount(&self, subperiod: Subperiod) -> Balance {
1053        self.current_stake_amount.for_type(subperiod)
1054    }
1055
1056    /// Total staked amount in the next era.
1057    pub fn total_staked_amount_next_era(&self) -> Balance {
1058        self.next_stake_amount.total()
1059    }
1060
1061    /// Staked amount of specified `type` in the next era.
1062    pub fn staked_amount_next_era(&self, subperiod: Subperiod) -> Balance {
1063        self.next_stake_amount.for_type(subperiod)
1064    }
1065
1066    /// Updates `Self` to reflect the transition to the next era.
1067    ///
1068    ///  ## Args
1069    /// `next_subperiod` - `None` if no subperiod change, `Some(type)` if `type` is starting from the next era.
1070    pub fn migrate_to_next_era(&mut self, next_subperiod: Option<Subperiod>) {
1071        match next_subperiod {
1072            // If next era marks start of new voting subperiod period, it means we're entering a new period
1073            Some(Subperiod::Voting) => {
1074                for stake_amount in [&mut self.current_stake_amount, &mut self.next_stake_amount] {
1075                    stake_amount.voting = Zero::zero();
1076                    stake_amount.build_and_earn = Zero::zero();
1077                    stake_amount.era.saturating_inc();
1078                    stake_amount.period.saturating_inc();
1079                }
1080            }
1081            Some(Subperiod::BuildAndEarn) | None => {
1082                self.current_stake_amount = self.next_stake_amount;
1083                self.next_stake_amount.era.saturating_inc();
1084            }
1085        };
1086    }
1087}
1088
1089/// Type alias for bonus status, where:
1090/// - `0` means the bonus is forfeited,
1091/// - `1` or greater means the staker is eligible for the bonus.
1092pub type BonusStatus = u8;
1093
1094/// Wrapper struct that provides additional methods for `BonusStatus`.
1095pub struct BonusStatusWrapper<MaxBonusMoves: Get<u8>>(BonusStatus, PhantomData<MaxBonusMoves>);
1096
1097impl<MaxBonusMoves: Get<u8>> Deref for BonusStatusWrapper<MaxBonusMoves> {
1098    type Target = BonusStatus;
1099
1100    fn deref(&self) -> &Self::Target {
1101        &self.0
1102    }
1103}
1104
1105impl<MaxBonusMoves: Get<u8>> Default for BonusStatusWrapper<MaxBonusMoves> {
1106    fn default() -> Self {
1107        let max = MaxBonusMoves::get();
1108        BonusStatusWrapper::<MaxBonusMoves>(max.saturating_add(1), PhantomData)
1109    }
1110}
1111
1112/// Information about how much a particular staker staked on a particular smart contract.
1113///
1114/// Keeps track of amount staked in the 'voting subperiod', as well as 'build&earn subperiod'.
1115#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
1116pub struct SingularStakingInfo {
1117    /// Amount staked before, if anything.
1118    pub(crate) previous_staked: StakeAmount,
1119    /// Staked amount
1120    pub(crate) staked: StakeAmount,
1121    /// Tracks the bonus eligibility: `0` means the bonus is forfeited, and `1` or greater indicates that the stake is eligible for bonus.
1122    /// Serves as counter for remaining safe moves based on `MaxBonusSafeMovesPerPeriod` value.
1123    pub(crate) bonus_status: BonusStatus,
1124}
1125
1126impl SingularStakingInfo {
1127    /// Creates new instance of the struct.
1128    ///
1129    /// ## Args
1130    ///
1131    /// `period` - period number for which this entry is relevant.
1132    /// `bonus_status` - `BonusStatus` to track bonus eligibility for this entry.
1133    pub(crate) fn new(period: PeriodNumber, bonus_status: BonusStatus) -> Self {
1134        Self {
1135            previous_staked: Default::default(),
1136            staked: StakeAmount {
1137                period,
1138                ..Default::default()
1139            },
1140            bonus_status,
1141        }
1142    }
1143
1144    /// Stake the specified amount on the contract.
1145    pub fn stake(
1146        &mut self,
1147        amount: StakeAmount,
1148        current_era: EraNumber,
1149        bonus_status: BonusStatus,
1150    ) {
1151        // Keep the previous stake amount for future reference
1152        if self.staked.era <= current_era {
1153            self.previous_staked = self.staked;
1154            self.previous_staked.era = current_era;
1155            if self.previous_staked.total().is_zero() {
1156                self.previous_staked = Default::default();
1157            }
1158        }
1159
1160        // This is necessary for move operations, when bonus is transferred to this own staking info
1161        if self.bonus_status == 0 {
1162            self.bonus_status = bonus_status;
1163        } else if self.bonus_status > 0 && bonus_status > 0 {
1164            let merged = (bonus_status + self.bonus_status) / 2;
1165            self.bonus_status = merged;
1166        }
1167
1168        // Stake is only valid from the next era so we keep it consistent here
1169        self.staked.add(amount.voting, Subperiod::Voting);
1170        self.staked
1171            .add(amount.build_and_earn, Subperiod::BuildAndEarn);
1172        self.staked.era = current_era.saturating_add(1);
1173    }
1174
1175    /// Unstakes some of the specified amount from the contract.
1176    ///
1177    /// In case the `amount` being unstaked is larger than the amount staked in the `Voting` subperiod,
1178    /// and `Voting` subperiod has passed, this will remove the _loyalty_ flag from the staker.
1179    ///
1180    /// Returns a vector of `(era, amount)` pairs, where `era` is the era in which the unstake happened,
1181    /// and the amount is the corresponding amount.
1182    ///
1183    /// ### NOTE
1184    /// `SingularStakingInfo` always aims to keep track of the staked amount between two consecutive eras.
1185    /// This means that the returned value will at most cover two eras - the last staked era, and the one before it.
1186    ///
1187    /// Last staked era can be the current era, or the era after.
1188    pub fn unstake(
1189        &mut self,
1190        amount: Balance,
1191        current_era: EraNumber,
1192        subperiod: Subperiod,
1193    ) -> (Vec<StakeAmount>, BonusStatus) {
1194        let mut result = Vec::new();
1195        let staked_snapshot = self.staked;
1196
1197        // 1. Modify 'current' staked amount.
1198        self.staked.subtract(amount);
1199        self.staked.era = self.staked.era.max(current_era);
1200
1201        let mut unstaked_amount = staked_snapshot.saturating_difference(&self.staked);
1202        unstaked_amount.era = self.staked.era;
1203
1204        // 2. Update bonus status accordingly.
1205        // In case voting subperiod has passed, and the 'voting' stake amount was reduced, we need to reduce the bonus eligibility counter.
1206        if subperiod != Subperiod::Voting && self.staked.voting < staked_snapshot.voting {
1207            self.bonus_status = self.bonus_status.saturating_sub(1);
1208        }
1209
1210        // Store the unstaked amount result
1211        result.push(unstaked_amount);
1212
1213        // 3. Determine what was the previous staked amount.
1214        // This is done by simply comparing where does the _previous era_ fit in the current context.
1215        let previous_era = self.staked.era.saturating_sub(1);
1216
1217        self.previous_staked = if staked_snapshot.era <= previous_era {
1218            let mut previous_staked = staked_snapshot;
1219            previous_staked.era = previous_era;
1220            previous_staked
1221        } else if !self.previous_staked.is_empty() && self.previous_staked.era <= previous_era {
1222            let mut previous_staked = self.previous_staked;
1223            previous_staked.era = previous_era;
1224            previous_staked
1225        } else {
1226            Default::default()
1227        };
1228
1229        // 4. Calculate how much is being unstaked from the previous staked era entry, in case its era equals the current era.
1230        //
1231        // Simples way to explain this is via an example.
1232        // Let's assume a simplification where stake amount entries are in `(era, amount)` format.
1233        //
1234        // a. Values: previous_staked: **(2, 10)**, staked: **(3, 15)**
1235        // b. User calls unstake during **era 2**, and unstakes amount **6**.
1236        //    Clearly some amount was staked during era 2, which resulted in era 3 stake being increased by 5.
1237        //    Calling unstake immediately in the same era should not necessarily reduce current era stake amount.
1238        //    This should be allowed to happen only if the unstaked amount is larger than the difference between the staked amount of two eras.
1239        // c. Values: previous_staked: **(2, 9)**, staked: **(3, 9)**
1240        //
1241        // An alternative scenario, where user calls unstake during **era 2**, and unstakes amount **4**.
1242        // c. Values: previous_staked: **(2, 10)**, staked: **(3, 11)**
1243        //
1244        // Note that the unstake operation didn't chip away from the current era, only the next one.
1245        if self.previous_staked.era == current_era {
1246            let maybe_stake_delta = staked_snapshot
1247                .total()
1248                .checked_sub(self.previous_staked.total());
1249            match maybe_stake_delta {
1250                Some(stake_delta) if unstaked_amount.total() > stake_delta => {
1251                    let overflow_amount = unstaked_amount.total() - stake_delta;
1252
1253                    let previous_staked_snapshot = self.previous_staked;
1254                    self.previous_staked.subtract(overflow_amount);
1255
1256                    let mut temp_unstaked_amount =
1257                        previous_staked_snapshot.saturating_difference(&self.previous_staked);
1258                    temp_unstaked_amount.era = self.previous_staked.era;
1259                    result.insert(0, temp_unstaked_amount);
1260                }
1261                _ => {}
1262            }
1263        } else if self.staked.era == current_era {
1264            // In case the `staked` era was already the current era, it also means we're chipping away from the future era.
1265            unstaked_amount.era = self.staked.era.saturating_add(1);
1266            result.push(unstaked_amount);
1267        }
1268
1269        // 5. Convenience cleanup
1270        if self.previous_staked.is_empty() {
1271            self.previous_staked = Default::default();
1272        }
1273        if self.staked.is_empty() {
1274            self.staked = Default::default();
1275            // No longer relevant.
1276            self.previous_staked = Default::default();
1277        }
1278
1279        (result, self.bonus_status)
1280    }
1281
1282    /// Total staked on the contract by the user. Both subperiod stakes are included.
1283    pub fn total_staked_amount(&self) -> Balance {
1284        self.staked.total()
1285    }
1286
1287    /// Returns amount staked in the specified period.
1288    pub fn staked_amount(&self, subperiod: Subperiod) -> Balance {
1289        self.staked.for_type(subperiod)
1290    }
1291
1292    /// If `true` staker has staked during voting subperiod and has never reduced their sta
1293    pub fn is_bonus_eligible(&self) -> bool {
1294        self.bonus_status > 0
1295    }
1296
1297    /// Period for which this entry is relevant.
1298    pub fn period_number(&self) -> PeriodNumber {
1299        self.staked.period
1300    }
1301
1302    /// Era in which the entry was last time updated
1303    pub fn era(&self) -> EraNumber {
1304        self.staked.era
1305    }
1306
1307    /// `true` if no stake exists, `false` otherwise.
1308    pub fn is_empty(&self) -> bool {
1309        self.staked.is_empty()
1310    }
1311}
1312
1313/// Composite type that holds information about how much was staked on a contract in up to two distinct eras.
1314///
1315/// This is needed since 'stake' operation only makes the staked amount valid from the next era.
1316/// In a situation when `stake` is called in era `N`, the staked amount is valid from era `N+1`, hence the need for 'future' entry.
1317///
1318/// **NOTE:** The 'future' entry term is only valid in the era when `stake` is called. It's possible contract stake isn't changed in consecutive eras,
1319/// so we might end up in a situation where era is `N + 10` but `staked` entry refers to era `N` and `staked_future` entry refers to era `N+1`.
1320/// This is still valid since these values are expected to be updated lazily.
1321#[derive(Encode, Decode, MaxEncodedLen, RuntimeDebug, PartialEq, Eq, Clone, TypeInfo, Default)]
1322pub struct ContractStakeAmount {
1323    /// Staked amount in the 'current' era.
1324    pub(crate) staked: StakeAmount,
1325    /// Staked amount in the next or 'future' era.
1326    pub(crate) staked_future: Option<StakeAmount>,
1327}
1328
1329impl ContractStakeAmount {
1330    /// `true` if series is empty, `false` otherwise.
1331    pub fn is_empty(&self) -> bool {
1332        self.staked.is_empty() && self.staked_future.is_none()
1333    }
1334
1335    /// Latest period for which stake entry exists.
1336    pub fn latest_stake_period(&self) -> Option<PeriodNumber> {
1337        if let Some(stake_amount) = self.staked_future {
1338            Some(stake_amount.period)
1339        } else if !self.staked.is_empty() {
1340            Some(self.staked.period)
1341        } else {
1342            None
1343        }
1344    }
1345
1346    /// Latest era for which stake entry exists.
1347    pub fn latest_stake_era(&self) -> Option<EraNumber> {
1348        if let Some(stake_amount) = self.staked_future {
1349            Some(stake_amount.era)
1350        } else if !self.staked.is_empty() {
1351            Some(self.staked.era)
1352        } else {
1353            None
1354        }
1355    }
1356
1357    /// Returns the `StakeAmount` type for the specified era & period, if it exists.
1358    pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option<StakeAmount> {
1359        let mut maybe_result = match (self.staked, self.staked_future) {
1360            (_, Some(staked_future)) if staked_future.era <= era => {
1361                if staked_future.period == period {
1362                    Some(staked_future)
1363                } else {
1364                    None
1365                }
1366            }
1367            (staked, _) if staked.era <= era && staked.period == period => Some(staked),
1368            _ => None,
1369        };
1370
1371        if let Some(result) = maybe_result.as_mut() {
1372            result.era = era;
1373        }
1374
1375        maybe_result
1376    }
1377
1378    /// Total staked amount on the contract, in the active period.
1379    pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance {
1380        match (self.staked, self.staked_future) {
1381            (_, Some(staked_future)) if staked_future.period == active_period => {
1382                staked_future.total()
1383            }
1384            (staked, _) if staked.period == active_period => staked.total(),
1385            _ => Balance::zero(),
1386        }
1387    }
1388
1389    /// Staked amount on the contract, for specified subperiod, in the active period.
1390    pub fn staked_amount(&self, active_period: PeriodNumber, subperiod: Subperiod) -> Balance {
1391        match (self.staked, self.staked_future) {
1392            (_, Some(staked_future)) if staked_future.period == active_period => {
1393                staked_future.for_type(subperiod)
1394            }
1395            (staked, _) if staked.period == active_period => staked.for_type(subperiod),
1396            _ => Balance::zero(),
1397        }
1398    }
1399
1400    /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`.
1401    pub fn stake(
1402        &mut self,
1403        amount: StakeAmount,
1404        current_era: EraNumber,
1405        period_number: PeriodNumber,
1406    ) {
1407        let stake_era = current_era.saturating_add(1);
1408
1409        match self.staked_future.as_mut() {
1410            // Future entry matches the era, just updated it and return
1411            Some(stake_amount) if stake_amount.era == stake_era => {
1412                stake_amount.add(amount.voting, Subperiod::Voting);
1413                stake_amount.add(amount.build_and_earn, Subperiod::BuildAndEarn);
1414                return;
1415            }
1416            // Future entry has an older era, but periods match so overwrite the 'current' entry with it
1417            Some(stake_amount) if stake_amount.period == period_number => {
1418                self.staked = *stake_amount;
1419                // Align the eras to keep it simple
1420                self.staked.era = current_era;
1421            }
1422            // Otherwise do nothing
1423            _ => (),
1424        }
1425
1426        // Prepare new entry
1427        let mut new_entry = match self.staked {
1428            // 'current' entry period matches so we use it as base for the new entry
1429            stake_amount if stake_amount.period == period_number => stake_amount,
1430            // otherwise just create a dummy new entry
1431            _ => Default::default(),
1432        };
1433        new_entry.add(amount.voting, Subperiod::Voting);
1434        new_entry.add(amount.build_and_earn, Subperiod::BuildAndEarn);
1435        new_entry.era = stake_era;
1436        new_entry.period = period_number;
1437
1438        self.staked_future = Some(new_entry);
1439
1440        // Convenience cleanup
1441        if self.staked.period < period_number {
1442            self.staked = Default::default();
1443        }
1444    }
1445
1446    /// Unstake the specified StakeAmount entries from the contract.
1447    // Important to account for the ongoing specified `subperiod` and `era` in order to align the entries.
1448    pub fn unstake(
1449        &mut self,
1450        stake_amount_entries: &Vec<StakeAmount>,
1451        period_info: PeriodInfo,
1452        current_era: EraNumber,
1453    ) {
1454        // 1. Entry alignment
1455        // We only need to keep track of the current era, and the next one.
1456        match self.staked_future {
1457            // Future entry exists, but it covers current or older era.
1458            Some(stake_amount)
1459                if stake_amount.era <= current_era && stake_amount.period == period_info.number =>
1460            {
1461                self.staked = stake_amount;
1462                self.staked.era = current_era;
1463                self.staked_future = None;
1464            }
1465            _ => (),
1466        }
1467
1468        // Current entry is from the right period, but older era. Shift it to the current era.
1469        if self.staked.era < current_era && self.staked.period == period_info.number {
1470            self.staked.era = current_era;
1471        }
1472
1473        // 2. Value updates - only after alignment
1474        for entry in stake_amount_entries {
1475            if self.staked.era == entry.era {
1476                self.staked.subtract_stake(&entry);
1477                continue;
1478            }
1479
1480            match self.staked_future.as_mut() {
1481                Some(future_stake_amount) if future_stake_amount.era == entry.era => {
1482                    future_stake_amount.subtract_stake(&entry);
1483                }
1484                // Otherwise do nothing
1485                _ => (),
1486            }
1487        }
1488
1489        // 3. Convenience cleanup
1490        if self.staked.is_empty() {
1491            self.staked = Default::default();
1492        }
1493        if let Some(stake_amount) = self.staked_future {
1494            if stake_amount.is_empty() {
1495                self.staked_future = None;
1496            }
1497        }
1498    }
1499}
1500
1501/// Information required for staker reward payout for a particular era.
1502#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo, Default)]
1503pub struct EraReward {
1504    /// Total reward pool for staker rewards
1505    #[codec(compact)]
1506    pub(crate) staker_reward_pool: Balance,
1507    /// Total amount which was staked at the end of an era
1508    #[codec(compact)]
1509    pub(crate) staked: Balance,
1510    /// Total reward pool for dApp rewards
1511    #[codec(compact)]
1512    pub(crate) dapp_reward_pool: Balance,
1513}
1514
1515impl EraReward {
1516    /// Total reward pool for staker rewards.
1517    pub fn staker_reward_pool(&self) -> Balance {
1518        self.staker_reward_pool
1519    }
1520
1521    /// Total amount which was staked at the end of an era.
1522    pub fn staked(&self) -> Balance {
1523        self.staked
1524    }
1525
1526    /// Total reward pool for dApp rewards
1527    pub fn dapp_reward_pool(&self) -> Balance {
1528        self.dapp_reward_pool
1529    }
1530}
1531
1532#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
1533pub enum EraRewardSpanError {
1534    /// Provided era is invalid. Must be exactly one era after the last one in the span.
1535    InvalidEra,
1536    /// Span has no more capacity for additional entries.
1537    NoCapacity,
1538}
1539
1540/// Used to efficiently store era span information.
1541#[derive(
1542    Encode,
1543    Decode,
1544    MaxEncodedLen,
1545    RuntimeDebugNoBound,
1546    PartialEqNoBound,
1547    DefaultNoBound,
1548    EqNoBound,
1549    CloneNoBound,
1550    TypeInfo,
1551)]
1552#[scale_info(skip_type_params(SL))]
1553pub struct EraRewardSpan<SL: Get<u32>> {
1554    /// Span of EraRewardInfo entries.
1555    pub(crate) span: BoundedVec<EraReward, SL>,
1556    /// The first era in the span.
1557    #[codec(compact)]
1558    first_era: EraNumber,
1559    /// The final era in the span.
1560    #[codec(compact)]
1561    last_era: EraNumber,
1562}
1563
1564impl<SL> EraRewardSpan<SL>
1565where
1566    SL: Get<u32>,
1567{
1568    /// Create new instance of the `EraRewardSpan`
1569    pub(crate) fn new() -> Self {
1570        Self {
1571            span: Default::default(),
1572            first_era: 0,
1573            last_era: 0,
1574        }
1575    }
1576
1577    /// First era covered in the span.
1578    pub fn first_era(&self) -> EraNumber {
1579        self.first_era
1580    }
1581
1582    /// Last era covered in the span
1583    pub fn last_era(&self) -> EraNumber {
1584        self.last_era
1585    }
1586
1587    /// Span length.
1588    pub fn len(&self) -> usize {
1589        self.span.len()
1590    }
1591
1592    /// `true` if span is empty, `false` otherwise.
1593    pub fn is_empty(&self) -> bool {
1594        self.span.is_empty()
1595    }
1596
1597    /// Push new `EraReward` entry into the span.
1598    /// If span is not empty, the provided `era` must be exactly one era after the last one in the span.
1599    pub fn push(
1600        &mut self,
1601        era: EraNumber,
1602        era_reward: EraReward,
1603    ) -> Result<(), EraRewardSpanError> {
1604        // First entry, no checks, just set eras to the provided value.
1605        if self.span.is_empty() {
1606            self.first_era = era;
1607            self.last_era = era;
1608            self.span
1609                .try_push(era_reward)
1610                // Defensive check, should never happen since it means capacity is 'zero'.
1611                .map_err(|_| EraRewardSpanError::NoCapacity)
1612        } else {
1613            // Defensive check to ensure next era rewards refers to era after the last one in the span.
1614            if era != self.last_era.saturating_add(1) {
1615                return Err(EraRewardSpanError::InvalidEra);
1616            }
1617
1618            self.last_era = era;
1619            self.span
1620                .try_push(era_reward)
1621                .map_err(|_| EraRewardSpanError::NoCapacity)
1622        }
1623    }
1624
1625    /// Get the `EraReward` entry for the specified `era`.
1626    ///
1627    /// In case `era` is not covered by the span, `None` is returned.
1628    pub fn get(&self, era: EraNumber) -> Option<&EraReward> {
1629        match era.checked_sub(self.first_era()) {
1630            Some(index) => self.span.get(index as usize),
1631            None => None,
1632        }
1633    }
1634}
1635
1636/// Description of tier entry requirement.
1637#[derive(
1638    Encode,
1639    Decode,
1640    DecodeWithMemTracking,
1641    MaxEncodedLen,
1642    Copy,
1643    Clone,
1644    Debug,
1645    PartialEq,
1646    Eq,
1647    TypeInfo,
1648    Serialize,
1649    Deserialize,
1650)]
1651pub enum TierThreshold {
1652    /// Entry into the tier is mandated by a fixed percentage of the total issuance as staked funds.
1653    /// This value is constant and does not change between periods.
1654    FixedPercentage { required_percentage: Perbill },
1655    /// Entry into the tier is mandated by a percentage of the total issuance as staked funds.
1656    /// This `percentage` can change between periods, but must stay within the defined
1657    /// `minimum_required_percentage` and `maximum_possible_percentage`.
1658    /// If minimum is greater than maximum, the configuration is invalid.
1659    ///
1660    /// NOTE: It's up to the user to ensure that minimum_required_percentage is
1661    /// less than or equal to maximum_possible_percentage to avoid potential issues.
1662    DynamicPercentage {
1663        percentage: Perbill,
1664        minimum_required_percentage: Perbill,
1665        maximum_possible_percentage: Perbill,
1666    },
1667}
1668
1669impl TierThreshold {
1670    /// Return threshold amount for the tier.
1671    pub fn threshold(&self, total_issuance: Balance) -> Balance {
1672        match self {
1673            Self::DynamicPercentage { percentage, .. } => *percentage * total_issuance,
1674            Self::FixedPercentage {
1675                required_percentage,
1676            } => *required_percentage * total_issuance,
1677        }
1678    }
1679}
1680
1681/// Top level description of tier slot parameters used to calculate tier configuration.
1682#[derive(
1683    Encode,
1684    Decode,
1685    DecodeWithMemTracking,
1686    MaxEncodedLen,
1687    RuntimeDebugNoBound,
1688    PartialEqNoBound,
1689    DefaultNoBound,
1690    EqNoBound,
1691    CloneNoBound,
1692    TypeInfo,
1693)]
1694#[scale_info(skip_type_params(NT))]
1695pub struct TierParameters<NT: Get<u32>> {
1696    /// Reward distribution per tier, in percentage.
1697    /// First entry refers to the first tier, and so on.
1698    /// The sum of all values must not exceed 100%.
1699    /// In case it is less, portion of rewards will never be distributed.
1700    pub(crate) reward_portion: BoundedVec<Permill, NT>,
1701    /// Distribution of number of slots per tier, in percentage.
1702    /// First entry refers to the first tier, and so on.
1703    /// The sum of all values must not exceed 100%.
1704    /// In case it is less, slot capacity will never be fully filled.
1705    pub(crate) slot_distribution: BoundedVec<Permill, NT>,
1706    /// Requirements for entry into each tier.
1707    /// First entry refers to the first tier, and so on.
1708    pub(crate) tier_thresholds: BoundedVec<TierThreshold, NT>,
1709    /// Arguments for the linear equation used to calculate the number of slots.
1710    /// This can be made more generic in the future in case more complex equations are required.
1711    /// But for now this simple tuple serves the purpose.
1712    pub(crate) slot_number_args: (u64, u64),
1713}
1714
1715impl<NT: Get<u32>> TierParameters<NT> {
1716    /// Check if configuration is valid.
1717    /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`.
1718    pub fn is_valid(&self) -> bool {
1719        // Reward portions sum should not exceed 100%.
1720        if self
1721            .reward_portion
1722            .iter()
1723            .fold(Some(Permill::zero()), |acc, permill| match acc {
1724                Some(acc) => acc.checked_add(permill),
1725                None => None,
1726            })
1727            .is_none()
1728        {
1729            return false;
1730        }
1731
1732        // Slot distribution sum should not exceed 100%.
1733        if self
1734            .slot_distribution
1735            .iter()
1736            .fold(Some(Permill::zero()), |acc, permill| match acc {
1737                Some(acc) => acc.checked_add(permill),
1738                None => None,
1739            })
1740            .is_none()
1741        {
1742            return false;
1743        }
1744
1745        // Validate that the minimum percentage is less than or equal to maximum percentage.
1746        for threshold in self.tier_thresholds.iter() {
1747            if let TierThreshold::DynamicPercentage {
1748                minimum_required_percentage,
1749                maximum_possible_percentage,
1750                ..
1751            } = threshold
1752            {
1753                if minimum_required_percentage > maximum_possible_percentage {
1754                    return false;
1755                }
1756            }
1757        }
1758
1759        let number_of_tiers: usize = NT::get() as usize;
1760        number_of_tiers == self.reward_portion.len()
1761            && number_of_tiers == self.slot_distribution.len()
1762            && number_of_tiers == self.tier_thresholds.len()
1763    }
1764}
1765
1766/// Configuration of dApp tiers.
1767#[derive(
1768    Encode,
1769    Decode,
1770    MaxEncodedLen,
1771    RuntimeDebugNoBound,
1772    PartialEqNoBound,
1773    DefaultNoBound,
1774    EqNoBound,
1775    CloneNoBound,
1776    TypeInfo,
1777)]
1778#[scale_info(skip_type_params(NT, T, P))]
1779pub struct TiersConfiguration<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> {
1780    /// Number of slots per tier.
1781    /// First entry refers to the first tier, and so on.
1782    pub(crate) slots_per_tier: BoundedVec<u16, NT>,
1783    /// Reward distribution per tier, in percentage.
1784    /// First entry refers to the first tier, and so on.
1785    /// The sum of all values must be exactly equal to 1.
1786    pub(crate) reward_portion: BoundedVec<Permill, NT>,
1787    /// Requirements for entry into each tier.
1788    /// First entry refers to the first tier, and so on.
1789    pub(crate) tier_thresholds: BoundedVec<Balance, NT>,
1790    /// Phantom data to keep track of the tier slots function.
1791    #[codec(skip)]
1792    pub(crate) _phantom: PhantomData<(T, P)>,
1793}
1794
1795impl<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> TiersConfiguration<NT, T, P> {
1796    /// Check if parameters are valid.
1797    pub fn is_valid(&self) -> bool {
1798        let number_of_tiers: usize = NT::get() as usize;
1799        number_of_tiers == self.slots_per_tier.len()
1800            // All vector length must match number of tiers.
1801            && number_of_tiers == self.reward_portion.len()
1802            && number_of_tiers == self.tier_thresholds.len()
1803    }
1804
1805    /// Calculate the total number of slots.
1806    pub fn total_number_of_slots(&self) -> u16 {
1807        self.slots_per_tier.iter().copied().sum()
1808    }
1809
1810    /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration.
1811    pub fn calculate_new(
1812        &self,
1813        params: &TierParameters<NT>,
1814        native_price: FixedU128,
1815        total_issuance: Balance,
1816    ) -> Self {
1817        // It must always be at least 1 slot.
1818        let base_number_of_slots = T::number_of_slots(P::get(), params.slot_number_args).max(1);
1819        let new_number_of_slots = T::number_of_slots(native_price, params.slot_number_args).max(1);
1820
1821        // Calculate how much each tier gets slots.
1822        let new_slots_per_tier: Vec<u16> = params
1823            .slot_distribution
1824            .clone()
1825            .into_inner()
1826            .iter()
1827            .map(|percent| *percent * new_number_of_slots as u128)
1828            .map(|x| x.unique_saturated_into())
1829            .collect();
1830        let new_slots_per_tier =
1831            BoundedVec::<u16, NT>::try_from(new_slots_per_tier).unwrap_or_default();
1832
1833        // NOTE: even though we could ignore the situation when the new & base slot numbers are equal, it's necessary to re-calculate it since
1834        // other params related to calculation might have changed.
1835        let delta_threshold = if new_number_of_slots >= base_number_of_slots {
1836            FixedU128::from_rational(
1837                (new_number_of_slots - base_number_of_slots).into(),
1838                new_number_of_slots.into(),
1839            )
1840        } else {
1841            FixedU128::from_rational(
1842                (base_number_of_slots - new_number_of_slots).into(),
1843                new_number_of_slots.into(),
1844            )
1845        };
1846
1847        // Update tier thresholds.
1848        // In case number of slots increase, we decrease thresholds required to enter the tier.
1849        // In case number of slots decrease, we increase the threshold required to enter the tier.
1850        //
1851        // According to formula: %delta_threshold = (100% / (100% - delta_%_slots) - 1) * 100%
1852        //
1853        // where delta_%_slots is simply: (base_num_slots - new_num_slots) / base_num_slots
1854        //
1855        // `base_num_slots` is the number of slots at the base native currency price.
1856        //
1857        // When these entries are put into the threshold formula, we get:
1858        // = 1 / ( 1 - (base_num_slots - new_num_slots) / base_num_slots ) - 1
1859        // = 1 / ( new / base) - 1
1860        // = base / new - 1
1861        // = (base - new) / new
1862        //
1863        // This number can be negative. In order to keep all operations in unsigned integer domain,
1864        // formulas are adjusted like:
1865        //
1866        // 1. Number of slots has increased, threshold is expected to decrease
1867        // %delta_threshold = (new_num_slots - base_num_slots) / new_num_slots
1868        // new_threshold = base_threshold * (1 - %delta_threshold)
1869        //
1870        // 2. Number of slots has decreased, threshold is expected to increase
1871        // %delta_threshold = (base_num_slots - new_num_slots) / new_num_slots
1872        // new_threshold = base_threshold * (1 + %delta_threshold)
1873        //
1874        let new_tier_thresholds: BoundedVec<Balance, NT> = params
1875            .tier_thresholds
1876            .clone()
1877            .iter()
1878            .map(|threshold| match threshold {
1879                TierThreshold::DynamicPercentage {
1880                    percentage,
1881                    minimum_required_percentage,
1882                    maximum_possible_percentage,
1883                } => {
1884                    let amount = *percentage * total_issuance;
1885                    let adjusted_amount = if new_number_of_slots >= base_number_of_slots {
1886                        amount.saturating_sub(delta_threshold.saturating_mul_int(amount))
1887                    } else {
1888                        amount.saturating_add(delta_threshold.saturating_mul_int(amount))
1889                    };
1890                    let minimum_amount = *minimum_required_percentage * total_issuance;
1891                    let maximum_amount = *maximum_possible_percentage * total_issuance;
1892                    adjusted_amount.max(minimum_amount).min(maximum_amount)
1893                }
1894                TierThreshold::FixedPercentage {
1895                    required_percentage,
1896                } => *required_percentage * total_issuance,
1897            })
1898            .collect::<Vec<_>>()
1899            .try_into()
1900            .unwrap_or_default();
1901
1902        Self {
1903            slots_per_tier: new_slots_per_tier,
1904            reward_portion: params.reward_portion.clone(),
1905            tier_thresholds: new_tier_thresholds,
1906            _phantom: Default::default(),
1907        }
1908    }
1909}
1910
1911/// Information about all of the dApps that got into tiers, and tier rewards
1912#[derive(
1913    Encode,
1914    Decode,
1915    MaxEncodedLen,
1916    RuntimeDebugNoBound,
1917    PartialEqNoBound,
1918    DefaultNoBound,
1919    EqNoBound,
1920    CloneNoBound,
1921    TypeInfo,
1922)]
1923#[scale_info(skip_type_params(MD, NT))]
1924pub struct DAppTierRewards<MD: Get<u32>, NT: Get<u32>> {
1925    /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime)
1926    pub(crate) dapps: BoundedBTreeMap<DAppId, RankedTier, MD>,
1927    /// Rewards for each tier. First entry refers to the first tier, and so on.
1928    pub(crate) rewards: BoundedVec<Balance, NT>,
1929    /// Period during which this struct was created.
1930    #[codec(compact)]
1931    pub(crate) period: PeriodNumber,
1932    /// Rank reward for each tier. First entry refers to the first tier, and so on.
1933    pub(crate) rank_rewards: BoundedVec<Balance, NT>,
1934}
1935
1936impl<MD: Get<u32>, NT: Get<u32>> DAppTierRewards<MD, NT> {
1937    /// Attempt to construct `DAppTierRewards` struct.
1938    /// If the provided arguments exceed the allowed capacity, return an error.
1939    pub(crate) fn new(
1940        dapps: BTreeMap<DAppId, RankedTier>,
1941        rewards: Vec<Balance>,
1942        period: PeriodNumber,
1943        rank_rewards: Vec<Balance>,
1944    ) -> Result<Self, ()> {
1945        let dapps = BoundedBTreeMap::try_from(dapps).map_err(|_| ())?;
1946        let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?;
1947        let rank_rewards = BoundedVec::try_from(rank_rewards).map_err(|_| ())?;
1948        Ok(Self {
1949            dapps,
1950            rewards,
1951            period,
1952            rank_rewards,
1953        })
1954    }
1955
1956    /// Consume reward for the specified dapp id, returning its amount and tier Id.
1957    /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`.
1958    pub fn try_claim(&mut self, dapp_id: DAppId) -> Result<(Balance, RankedTier), DAppTierError> {
1959        // Check if dApp Id exists.
1960        let ranked_tier = self
1961            .dapps
1962            .remove(&dapp_id)
1963            .ok_or(DAppTierError::NoDAppInTiers)?;
1964
1965        let (tier_id, rank) = ranked_tier.deconstruct();
1966        let mut amount = self
1967            .rewards
1968            .get(tier_id as usize)
1969            .map_or(Balance::zero(), |x| *x);
1970
1971        let reward_per_rank = self
1972            .rank_rewards
1973            .get(tier_id as usize)
1974            .map_or(Balance::zero(), |x| *x);
1975
1976        let additional_reward = reward_per_rank.saturating_mul(rank.into());
1977        amount = amount.saturating_add(additional_reward);
1978
1979        Ok((amount, ranked_tier))
1980    }
1981}
1982
1983#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1984pub enum DAppTierError {
1985    /// Specified dApp Id doesn't exist in any tier.
1986    NoDAppInTiers,
1987    /// Internal, unexpected error occurred.
1988    InternalError,
1989}
1990
1991/// Describes which entries are next in line for cleanup.
1992#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
1993pub struct CleanupMarker {
1994    /// Era reward span index that should be checked & cleaned up next.
1995    #[codec(compact)]
1996    pub(crate) era_reward_index: EraNumber,
1997    /// dApp tier rewards index that should be checked & cleaned up next.
1998    #[codec(compact)]
1999    pub(crate) dapp_tiers_index: EraNumber,
2000    /// Oldest valid era or earliest era in the oldest valid period.
2001    #[codec(compact)]
2002    pub(crate) oldest_valid_era: EraNumber,
2003}
2004
2005impl CleanupMarker {
2006    /// Used to check whether there are any pending cleanups, according to marker values.
2007    pub(crate) fn has_pending_cleanups(&self) -> bool {
2008        self.era_reward_index != self.oldest_valid_era
2009            || self.dapp_tiers_index != self.oldest_valid_era
2010    }
2011}