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, ¤t_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, ¤t_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}