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