pallet_dapp_staking/
lib.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 v3 Pallet
20//!
21//! For detailed high level documentation, please refer to the attached README.md file.
22//! The crate level docs will cover overall pallet structure & implementation details.
23//!
24//! ## Overview
25//!
26//! Pallet that implements the dApp staking v3 protocol.
27//! It covers everything from locking, staking, tier configuration & assignment, reward calculation & payout.
28//!
29//! The `types` module contains all of the types used to implement the pallet.
30//! All of these _types_ are extensively tested in their dedicated `test_types` module.
31//!
32//! Rest of the pallet logic is concentrated in the lib.rs file.
33//! This logic is tested in the `tests` module, with the help of extensive `testing_utils`.
34//!
35
36#![cfg_attr(not(feature = "std"), no_std)]
37
38use frame_support::{
39    pallet_prelude::*,
40    traits::{
41        fungible::{Inspect as FunInspect, MutateFreeze as FunMutateFreeze},
42        SafeModeNotify, StorageVersion,
43    },
44    weights::Weight,
45};
46use frame_system::pallet_prelude::*;
47use sp_arithmetic::fixed_point::FixedU128;
48use sp_runtime::{
49    traits::{One, Saturating, UniqueSaturatedInto, Zero},
50    Perbill, Permill, SaturatedConversion,
51};
52pub use sp_std::vec::Vec;
53
54use astar_primitives::{
55    dapp_staking::{
56        AccountCheck, CycleConfiguration, DAppId, EraNumber, Observer as DAppStakingObserver,
57        PeriodNumber, Rank, RankedTier, SmartContractHandle, StakingRewardHandler, TierId,
58        TierSlots as TierSlotFunc, STANDARD_TIER_SLOTS_ARGS,
59    },
60    oracle::PriceProvider,
61    Balance, BlockNumber,
62};
63
64pub use pallet::*;
65
66#[cfg(test)]
67mod test;
68
69#[cfg(feature = "runtime-benchmarks")]
70mod benchmarking;
71
72mod types;
73pub use types::*;
74
75pub mod migration;
76pub mod weights;
77
78pub use weights::WeightInfo;
79
80const LOG_TARGET: &str = "dapp-staking";
81
82/// Helper enum for benchmarking.
83pub(crate) enum TierAssignment {
84    /// Real tier assignment calculation should be done.
85    Real,
86    /// Dummy tier assignment calculation should be done, e.g. default value should be returned.
87    #[cfg(feature = "runtime-benchmarks")]
88    Dummy,
89}
90
91#[doc = include_str!("../README.md")]
92#[frame_support::pallet]
93pub mod pallet {
94    use super::*;
95
96    /// The current storage version.
97    pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(10);
98
99    #[pallet::pallet]
100    #[pallet::storage_version(STORAGE_VERSION)]
101    pub struct Pallet<T>(_);
102
103    #[cfg(feature = "runtime-benchmarks")]
104    pub trait BenchmarkHelper<SmartContract, AccountId> {
105        fn get_smart_contract(id: u32) -> SmartContract;
106
107        fn set_balance(account: &AccountId, balance: Balance);
108    }
109
110    #[pallet::config]
111    pub trait Config: frame_system::Config {
112        /// The overarching event type.
113        #[allow(deprecated)]
114        type RuntimeEvent: From<Event<Self>>
115            + IsType<<Self as frame_system::Config>::RuntimeEvent>
116            + TryInto<Event<Self>>;
117
118        /// The overarching freeze reason.
119        type RuntimeFreezeReason: From<FreezeReason>;
120
121        /// Currency used for staking.
122        /// Reference: <https://github.com/paritytech/substrate/pull/12951/>
123        type Currency: FunMutateFreeze<
124            Self::AccountId,
125            Id = Self::RuntimeFreezeReason,
126            Balance = Balance,
127        >;
128
129        /// Describes smart contract in the context required by dApp staking.
130        type SmartContract: Parameter
131            + Member
132            + MaxEncodedLen
133            + SmartContractHandle<Self::AccountId>;
134
135        /// Privileged origin that is allowed to register smart contracts to the protocol.
136        type ContractRegisterOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
137
138        /// Privileged origin that is allowed to unregister smart contracts from the protocol.
139        type ContractUnregisterOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
140
141        /// Privileged origin for managing dApp staking pallet.
142        type ManagerOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
143
144        /// Used to provide price information about the native token.
145        type NativePriceProvider: PriceProvider;
146
147        /// Used to handle reward payouts & reward pool amount fetching.
148        type StakingRewardHandler: StakingRewardHandler<Self::AccountId>;
149
150        /// Describes era length, subperiods & period length, as well as cycle length.
151        type CycleConfiguration: CycleConfiguration;
152
153        /// dApp staking event observers, notified when certain events occur.
154        type Observers: DAppStakingObserver;
155
156        /// Used to check whether an account is allowed to participate in dApp staking.
157        type AccountCheck: AccountCheck<Self::AccountId>;
158
159        /// Used to calculate total number of tier slots for some price.
160        type TierSlots: TierSlotFunc;
161
162        /// Base native currency price used to calculate base number of slots.
163        /// This is used to adjust tier configuration, tier thresholds specifically, based on the native token price changes.
164        ///
165        /// When dApp staking thresholds were modeled, a base price was set from which the initial configuration is derived.
166        /// E.g. for a price of 0.05$, we get 100 slots, and certain tier thresholds.
167        /// Using these values as the base, we can adjust the configuration based on the current price.
168        ///
169        /// This is connected with the `TierSlots` associated type, since it's used to calculate the total number of slots for the given price.
170        #[pallet::constant]
171        type BaseNativeCurrencyPrice: Get<FixedU128>;
172
173        /// Maximum length of a single era reward span length entry.
174        #[pallet::constant]
175        type EraRewardSpanLength: Get<u32>;
176
177        /// Number of periods for which we keep rewards available for claiming.
178        /// After that period, they are no longer claimable.
179        #[pallet::constant]
180        type RewardRetentionInPeriods: Get<PeriodNumber>;
181
182        /// Maximum number of contracts that can be integrated into dApp staking at once.
183        #[pallet::constant]
184        type MaxNumberOfContracts: Get<u32>;
185
186        /// Maximum number of unlocking chunks that can exist per account at a time.
187        #[pallet::constant]
188        type MaxUnlockingChunks: Get<u32>;
189
190        /// Minimum amount an account has to lock in dApp staking in order to participate.
191        #[pallet::constant]
192        type MinimumLockedAmount: Get<Balance>;
193
194        /// Number of standard eras that need to pass before unlocking chunk can be claimed.
195        /// Even though it's expressed in 'eras', it's actually measured in number of blocks.
196        #[pallet::constant]
197        type UnlockingPeriod: Get<EraNumber>;
198
199        /// Maximum amount of stake contract entries an account is allowed to have at once.
200        #[pallet::constant]
201        type MaxNumberOfStakedContracts: Get<u32>;
202
203        /// Minimum amount staker can stake on a contract.
204        #[pallet::constant]
205        type MinimumStakeAmount: Get<Balance>;
206
207        /// Number of different tiers.
208        #[pallet::constant]
209        type NumberOfTiers: Get<u32>;
210
211        /// Tier ranking enabled.
212        #[pallet::constant]
213        type RankingEnabled: Get<bool>;
214
215        /// The maximum number of 'safe move actions' allowed within a single period while
216        /// retaining eligibility for bonus rewards. Exceeding this limit will result in the
217        /// forfeiture of the bonus rewards for the affected stake.
218        #[pallet::constant]
219        type MaxBonusSafeMovesPerPeriod: Get<u8>;
220
221        /// Weight info for various calls & operations in the pallet.
222        type WeightInfo: WeightInfo;
223
224        /// Helper trait for benchmarks.
225        #[cfg(feature = "runtime-benchmarks")]
226        type BenchmarkHelper: BenchmarkHelper<Self::SmartContract, Self::AccountId>;
227    }
228
229    #[pallet::event]
230    #[pallet::generate_deposit(pub(crate) fn deposit_event)]
231    pub enum Event<T: Config> {
232        /// Maintenance mode has been either enabled or disabled.
233        MaintenanceMode { enabled: bool },
234        /// New era has started.
235        NewEra { era: EraNumber },
236        /// New subperiod has started.
237        NewSubperiod {
238            subperiod: Subperiod,
239            number: PeriodNumber,
240        },
241        /// A smart contract has been registered for dApp staking
242        DAppRegistered {
243            owner: T::AccountId,
244            smart_contract: T::SmartContract,
245            dapp_id: DAppId,
246        },
247        /// dApp reward destination has been updated.
248        DAppRewardDestinationUpdated {
249            smart_contract: T::SmartContract,
250            beneficiary: Option<T::AccountId>,
251        },
252        /// dApp owner has been changed.
253        DAppOwnerChanged {
254            smart_contract: T::SmartContract,
255            new_owner: T::AccountId,
256        },
257        /// dApp has been unregistered
258        DAppUnregistered {
259            smart_contract: T::SmartContract,
260            era: EraNumber,
261        },
262        /// Account has locked some amount into dApp staking.
263        Locked {
264            account: T::AccountId,
265            amount: Balance,
266        },
267        /// Account has started the unlocking process for some amount.
268        Unlocking {
269            account: T::AccountId,
270            amount: Balance,
271        },
272        /// Account has claimed unlocked amount, removing the lock from it.
273        ClaimedUnlocked {
274            account: T::AccountId,
275            amount: Balance,
276        },
277        /// Account has relocked all of the unlocking chunks.
278        Relock {
279            account: T::AccountId,
280            amount: Balance,
281        },
282        /// Account has staked some amount on a smart contract.
283        Stake {
284            account: T::AccountId,
285            smart_contract: T::SmartContract,
286            amount: Balance,
287        },
288        /// Account has unstaked some amount from a smart contract.
289        Unstake {
290            account: T::AccountId,
291            smart_contract: T::SmartContract,
292            amount: Balance,
293        },
294        /// Account has claimed some stake rewards.
295        Reward {
296            account: T::AccountId,
297            era: EraNumber,
298            amount: Balance,
299        },
300        /// Bonus reward has been paid out to a staker with an eligible bonus status.
301        BonusReward {
302            account: T::AccountId,
303            smart_contract: T::SmartContract,
304            period: PeriodNumber,
305            amount: Balance,
306        },
307        /// dApp reward has been paid out to a beneficiary.
308        DAppReward {
309            beneficiary: T::AccountId,
310            smart_contract: T::SmartContract,
311            tier_id: TierId,
312            rank: Rank,
313            era: EraNumber,
314            amount: Balance,
315        },
316        /// Account has unstaked funds from an unregistered smart contract
317        UnstakeFromUnregistered {
318            account: T::AccountId,
319            smart_contract: T::SmartContract,
320            amount: Balance,
321        },
322        /// Some expired stake entries have been removed from storage.
323        ExpiredEntriesRemoved { account: T::AccountId, count: u16 },
324        /// Privileged origin has forced a new era and possibly a subperiod to start from next block.
325        Force { forcing_type: ForcingType },
326        /// Account has moved some stake from a source smart contract to a destination smart contract.
327        StakeMoved {
328            account: T::AccountId,
329            source_contract: T::SmartContract,
330            destination_contract: T::SmartContract,
331            amount: Balance,
332        },
333        /// Tier parameters, used to calculate tier configuration, have been updated, and will be applicable from next era.
334        NewTierParameters {
335            params: TierParameters<T::NumberOfTiers>,
336        },
337    }
338
339    #[pallet::error]
340    pub enum Error<T> {
341        /// Pallet is disabled/in maintenance mode.
342        Disabled,
343        /// Smart contract already exists within dApp staking protocol.
344        ContractAlreadyExists,
345        /// Maximum number of smart contracts has been reached.
346        ExceededMaxNumberOfContracts,
347        /// Not possible to assign a new dApp Id.
348        /// This should never happen since current type can support up to 65536 - 1 unique dApps.
349        NewDAppIdUnavailable,
350        /// Specified smart contract does not exist in dApp staking.
351        ContractNotFound,
352        /// Call origin is not dApp owner.
353        OriginNotOwner,
354        /// Performing locking or staking with 0 amount.
355        ZeroAmount,
356        /// Total locked amount for staker is below minimum threshold.
357        LockedAmountBelowThreshold,
358        /// Account is not allowed to participate in dApp staking due to some external reason (e.g. account is already a collator).
359        AccountNotAvailableForDappStaking,
360        /// Cannot add additional unlocking chunks due to capacity limit.
361        TooManyUnlockingChunks,
362        /// Remaining stake prevents entire balance of starting the unlocking process.
363        RemainingStakePreventsFullUnlock,
364        /// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all.
365        NoUnlockedChunksToClaim,
366        /// There are no unlocking chunks available to relock.
367        NoUnlockingChunks,
368        /// The amount being staked is too large compared to what's available for staking.
369        UnavailableStakeFunds,
370        /// There are unclaimed rewards remaining from past eras or periods. They should be claimed before attempting any stake modification again.
371        UnclaimedRewards,
372        /// An unexpected error occurred while trying to stake.
373        InternalStakeError,
374        /// Total staked amount on contract is below the minimum required value.
375        InsufficientStakeAmount,
376        /// Stake operation is rejected since period ends in the next era.
377        PeriodEndsInNextEra,
378        /// Unstaking is rejected since the period in which past stake was active has passed.
379        UnstakeFromPastPeriod,
380        /// Unstake amount is greater than the staked amount.
381        UnstakeAmountTooLarge,
382        /// Account has no staking information for the contract.
383        NoStakingInfo,
384        /// An unexpected error occurred while trying to unstake.
385        InternalUnstakeError,
386        /// Rewards are no longer claimable since they are too old.
387        RewardExpired,
388        /// Reward payout has failed due to an unexpected reason.
389        RewardPayoutFailed,
390        /// There are no claimable rewards.
391        NoClaimableRewards,
392        /// An unexpected error occurred while trying to claim staker rewards.
393        InternalClaimStakerError,
394        /// Account is has no eligible stake amount for bonus reward.
395        NotEligibleForBonusReward,
396        /// An unexpected error occurred while trying to claim bonus reward.
397        InternalClaimBonusError,
398        /// Claim era is invalid - it must be in history, and rewards must exist for it.
399        InvalidClaimEra,
400        /// No dApp tier info exists for the specified era. This can be because era has expired
401        /// or because during the specified era there were no eligible rewards or protocol wasn't active.
402        NoDAppTierInfo,
403        /// An unexpected error occurred while trying to claim dApp reward.
404        InternalClaimDAppError,
405        /// Contract is still active, not unregistered.
406        ContractStillActive,
407        /// There are too many contract stake entries for the account. This can be cleaned up by either unstaking or cleaning expired entries.
408        TooManyStakedContracts,
409        /// There are no expired entries to cleanup for the account.
410        NoExpiredEntries,
411        /// Force call is not allowed in production.
412        ForceNotAllowed,
413        /// Invalid tier parameters were provided. This can happen if any number exceeds 100% or if number of elements does not match the number of tiers.
414        InvalidTierParams,
415        /// Same contract specified as source and destination.
416        SameContracts,
417    }
418
419    /// General information about dApp staking protocol state.
420    #[pallet::storage]
421    #[pallet::whitelist_storage]
422    pub type ActiveProtocolState<T: Config> = StorageValue<_, ProtocolState, ValueQuery>;
423
424    /// Counter for unique dApp identifiers.
425    #[pallet::storage]
426    pub type NextDAppId<T: Config> = StorageValue<_, DAppId, ValueQuery>;
427
428    /// Map of all dApps integrated into dApp staking protocol.
429    ///
430    /// Even though dApp is integrated, it does not mean it's still actively participating in dApp staking.
431    /// It might have been unregistered at some point in history.
432    #[pallet::storage]
433    pub type IntegratedDApps<T: Config> = CountedStorageMap<
434        Hasher = Blake2_128Concat,
435        Key = T::SmartContract,
436        Value = DAppInfo<T::AccountId>,
437        QueryKind = OptionQuery,
438        MaxValues = ConstU32<{ DAppId::MAX as u32 }>,
439    >;
440
441    /// General locked/staked information for each account.
442    #[pallet::storage]
443    pub type Ledger<T: Config> =
444        StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor<T>, ValueQuery>;
445
446    /// Information about how much each staker has staked for each smart contract in some period.
447    #[pallet::storage]
448    pub type StakerInfo<T: Config> = StorageDoubleMap<
449        _,
450        Blake2_128Concat,
451        T::AccountId,
452        Blake2_128Concat,
453        T::SmartContract,
454        SingularStakingInfo,
455        OptionQuery,
456    >;
457
458    /// Information about how much has been staked on a smart contract in some era or period.
459    #[pallet::storage]
460    pub type ContractStake<T: Config> = StorageMap<
461        Hasher = Twox64Concat,
462        Key = DAppId,
463        Value = ContractStakeAmount,
464        QueryKind = ValueQuery,
465        MaxValues = ConstU32<{ DAppId::MAX as u32 }>,
466    >;
467
468    /// General information about the current era.
469    #[pallet::storage]
470    pub type CurrentEraInfo<T: Config> = StorageValue<_, EraInfo, ValueQuery>;
471
472    /// Information about rewards for each era.
473    ///
474    /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage.
475    /// For the sake of simplicity, valid `era` keys are calculated as:
476    ///
477    /// era_key = era - (era % T::EraRewardSpanLength)
478    ///
479    /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage.
480    /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc.
481    #[pallet::storage]
482    pub type EraRewards<T: Config> =
483        StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan<T::EraRewardSpanLength>, OptionQuery>;
484
485    /// Information about period's end.
486    #[pallet::storage]
487    pub type PeriodEnd<T: Config> =
488        StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>;
489
490    /// Static tier parameters used to calculate tier configuration.
491    #[pallet::storage]
492    pub type StaticTierParams<T: Config> =
493        StorageValue<_, TierParameters<T::NumberOfTiers>, ValueQuery>;
494
495    /// Tier configuration user for current & preceding eras.
496    #[pallet::storage]
497    pub type TierConfig<T: Config> = StorageValue<
498        _,
499        TiersConfiguration<T::NumberOfTiers, T::TierSlots, T::BaseNativeCurrencyPrice>,
500        ValueQuery,
501    >;
502
503    /// Information about which tier a dApp belonged to in a specific era.
504    #[pallet::storage]
505    pub type DAppTiers<T: Config> =
506        StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor<T>, OptionQuery>;
507
508    /// History cleanup marker - holds information about which DB entries should be cleaned up next, when applicable.
509    #[pallet::storage]
510    pub type HistoryCleanupMarker<T: Config> = StorageValue<_, CleanupMarker, ValueQuery>;
511
512    #[pallet::type_value]
513    pub fn DefaultSafeguard<T: Config>() -> bool {
514        // In production, safeguard is enabled by default.
515        // Safeguard can be disabled per chain via Genesis Config.
516        true
517    }
518
519    /// Safeguard to prevent unwanted operations in production.
520    /// Kept as a storage without extrinsic setter, so we can still enable it for some
521    /// chain-fork debugging if required.
522    #[pallet::storage]
523    pub type Safeguard<T: Config> = StorageValue<_, bool, ValueQuery, DefaultSafeguard<T>>;
524
525    #[pallet::genesis_config]
526    pub struct GenesisConfig<T: Config> {
527        pub reward_portion: Vec<Permill>,
528        pub slot_distribution: Vec<Permill>,
529        pub tier_thresholds: Vec<TierThreshold>,
530        pub slot_number_args: (u64, u64),
531        pub slots_per_tier: Vec<u16>,
532        pub safeguard: Option<bool>,
533        #[serde(skip)]
534        pub _config: PhantomData<T>,
535    }
536
537    impl<T: Config> Default for GenesisConfig<T> {
538        fn default() -> Self {
539            use sp_std::vec;
540            let num_tiers = T::NumberOfTiers::get();
541            Self {
542                reward_portion: vec![Permill::from_percent(100 / num_tiers); num_tiers as usize],
543                slot_distribution: vec![Permill::from_percent(100 / num_tiers); num_tiers as usize],
544                tier_thresholds: (0..num_tiers)
545                    .rev()
546                    .map(|i| TierThreshold::FixedPercentage {
547                        required_percentage: Perbill::from_percent(i),
548                    })
549                    .collect(),
550                slot_number_args: STANDARD_TIER_SLOTS_ARGS,
551                slots_per_tier: vec![100; num_tiers as usize],
552                safeguard: None,
553                _config: Default::default(),
554            }
555        }
556    }
557
558    #[pallet::genesis_build]
559    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
560        fn build(&self) {
561            // Prepare tier parameters & verify their correctness
562            let tier_params = TierParameters::<T::NumberOfTiers> {
563                reward_portion: BoundedVec::<Permill, T::NumberOfTiers>::try_from(
564                    self.reward_portion.clone(),
565                )
566                .expect("Invalid number of reward portions provided."),
567                slot_distribution: BoundedVec::<Permill, T::NumberOfTiers>::try_from(
568                    self.slot_distribution.clone(),
569                )
570                .expect("Invalid number of slot distributions provided."),
571                tier_thresholds: BoundedVec::<TierThreshold, T::NumberOfTiers>::try_from(
572                    self.tier_thresholds.clone(),
573                )
574                .expect("Invalid number of tier thresholds provided."),
575                slot_number_args: self.slot_number_args,
576            };
577            assert!(
578                tier_params.is_valid(),
579                "Invalid tier parameters values provided."
580            );
581
582            let total_issuance = T::Currency::total_issuance();
583            let tier_thresholds = tier_params
584                .tier_thresholds
585                .iter()
586                .map(|t| t.threshold(total_issuance))
587                .collect::<Vec<Balance>>()
588                .try_into()
589                .expect("Invalid number of tier thresholds provided.");
590
591            let tier_config =
592                TiersConfiguration::<T::NumberOfTiers, T::TierSlots, T::BaseNativeCurrencyPrice> {
593                    slots_per_tier: BoundedVec::<u16, T::NumberOfTiers>::try_from(
594                        self.slots_per_tier.clone(),
595                    )
596                    .expect("Invalid number of slots per tier entries provided."),
597                    reward_portion: tier_params.reward_portion.clone(),
598                    tier_thresholds,
599                    _phantom: Default::default(),
600                };
601            assert!(
602                tier_config.is_valid(),
603                "Invalid tier config values provided."
604            );
605
606            // Prepare initial protocol state
607            let protocol_state = ProtocolState {
608                era: 1,
609                next_era_start: Pallet::<T>::blocks_per_voting_period()
610                    .checked_add(1)
611                    .expect("Must not overflow - especially not at genesis."),
612                period_info: PeriodInfo {
613                    number: 1,
614                    subperiod: Subperiod::Voting,
615                    next_subperiod_start_era: 2,
616                },
617                maintenance: false,
618            };
619
620            // Initialize necessary storage items
621            ActiveProtocolState::<T>::put(protocol_state);
622            StaticTierParams::<T>::put(tier_params);
623            TierConfig::<T>::put(tier_config.clone());
624
625            if self.safeguard.is_some() {
626                Safeguard::<T>::put(self.safeguard.unwrap());
627            }
628        }
629    }
630
631    #[pallet::hooks]
632    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
633        fn on_initialize(now: BlockNumberFor<T>) -> Weight {
634            let now = now.saturated_into();
635            Self::era_and_period_handler(now, TierAssignment::Real)
636        }
637
638        fn on_idle(_block: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
639            Self::expired_entry_cleanup(&remaining_weight)
640        }
641
642        fn integrity_test() {
643            // dApp staking params
644            // Sanity checks
645            assert!(T::EraRewardSpanLength::get() > 0);
646            assert!(T::RewardRetentionInPeriods::get() > 0);
647            assert!(T::MaxNumberOfContracts::get() > 0);
648            assert!(T::MaxUnlockingChunks::get() > 0);
649            assert!(T::UnlockingPeriod::get() > 0);
650            assert!(T::MaxNumberOfStakedContracts::get() > 0);
651
652            assert!(T::MinimumLockedAmount::get() > 0);
653            assert!(T::MinimumStakeAmount::get() > 0);
654            assert!(T::MinimumLockedAmount::get() >= T::MinimumStakeAmount::get());
655
656            // Cycle config
657            assert!(T::CycleConfiguration::periods_per_cycle() > 0);
658            assert!(T::CycleConfiguration::eras_per_voting_subperiod() > 0);
659            assert!(T::CycleConfiguration::eras_per_build_and_earn_subperiod() > 0);
660            assert!(T::CycleConfiguration::blocks_per_era() > 0);
661        }
662
663        #[cfg(feature = "try-runtime")]
664        fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
665            Self::do_try_state()?;
666            Ok(())
667        }
668    }
669
670    /// A reason for freezing funds.
671    #[pallet::composite_enum]
672    pub enum FreezeReason {
673        /// Account is participating in dApp staking.
674        #[codec(index = 0)]
675        DAppStaking,
676    }
677
678    #[pallet::call]
679    impl<T: Config> Pallet<T> {
680        /// Wrapper around _legacy-like_ `unbond_and_unstake`.
681        ///
682        /// Used to support legacy Ledger users so they can start the unlocking process for their funds.
683        #[pallet::call_index(4)]
684        #[pallet::weight(T::WeightInfo::unlock())]
685        pub fn unbond_and_unstake(
686            origin: OriginFor<T>,
687            _contract_id: T::SmartContract,
688            #[pallet::compact] value: Balance,
689        ) -> DispatchResult {
690            // Once new period begins, all stakes are reset to zero, so all it remains to be done is the `unlock`
691            Self::unlock(origin, value)
692        }
693
694        /// Wrapper around _legacy-like_ `withdraw_unbonded`.
695        ///
696        /// Used to support legacy Ledger users so they can reclaim unlocked chunks back into
697        /// their _transferable_ free balance.
698        #[pallet::call_index(5)]
699        #[pallet::weight(T::WeightInfo::claim_unlocked(T::MaxNumberOfStakedContracts::get()))]
700        pub fn withdraw_unbonded(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
701            Self::claim_unlocked(origin)
702        }
703
704        /// Used to enable or disable maintenance mode.
705        /// Can only be called by manager origin.
706        #[pallet::call_index(0)]
707        #[pallet::weight(T::WeightInfo::maintenance_mode())]
708        pub fn maintenance_mode(origin: OriginFor<T>, enabled: bool) -> DispatchResult {
709            T::ManagerOrigin::ensure_origin(origin)?;
710            Self::set_maintenance_mode(enabled);
711            Ok(())
712        }
713
714        /// Used to register a new contract for dApp staking.
715        ///
716        /// If successful, smart contract will be assigned a simple, unique numerical identifier.
717        /// Owner is set to be initial beneficiary & manager of the dApp.
718        #[pallet::call_index(1)]
719        #[pallet::weight(T::WeightInfo::register())]
720        pub fn register(
721            origin: OriginFor<T>,
722            owner: T::AccountId,
723            smart_contract: T::SmartContract,
724        ) -> DispatchResult {
725            Self::ensure_pallet_enabled()?;
726            T::ContractRegisterOrigin::ensure_origin(origin)?;
727
728            ensure!(
729                !IntegratedDApps::<T>::contains_key(&smart_contract),
730                Error::<T>::ContractAlreadyExists,
731            );
732
733            ensure!(
734                IntegratedDApps::<T>::count() < T::MaxNumberOfContracts::get().into(),
735                Error::<T>::ExceededMaxNumberOfContracts
736            );
737
738            let dapp_id = NextDAppId::<T>::get();
739            // MAX value must never be assigned as a dApp Id since it serves as a sentinel value.
740            ensure!(dapp_id < DAppId::MAX, Error::<T>::NewDAppIdUnavailable);
741
742            IntegratedDApps::<T>::insert(
743                &smart_contract,
744                DAppInfo {
745                    owner: owner.clone(),
746                    id: dapp_id,
747                    reward_beneficiary: None,
748                },
749            );
750
751            NextDAppId::<T>::put(dapp_id.saturating_add(1));
752
753            Self::deposit_event(Event::<T>::DAppRegistered {
754                owner,
755                smart_contract,
756                dapp_id,
757            });
758
759            Ok(())
760        }
761
762        /// Used to modify the reward beneficiary account for a dApp.
763        ///
764        /// Caller has to be dApp owner.
765        /// If set to `None`, rewards will be deposited to the dApp owner.
766        /// After this call, all existing & future rewards will be paid out to the beneficiary.
767        #[pallet::call_index(2)]
768        #[pallet::weight(T::WeightInfo::set_dapp_reward_beneficiary())]
769        pub fn set_dapp_reward_beneficiary(
770            origin: OriginFor<T>,
771            smart_contract: T::SmartContract,
772            beneficiary: Option<T::AccountId>,
773        ) -> DispatchResult {
774            Self::ensure_pallet_enabled()?;
775            let dev_account = ensure_signed(origin)?;
776
777            IntegratedDApps::<T>::try_mutate(
778                &smart_contract,
779                |maybe_dapp_info| -> DispatchResult {
780                    let dapp_info = maybe_dapp_info
781                        .as_mut()
782                        .ok_or(Error::<T>::ContractNotFound)?;
783
784                    ensure!(dapp_info.owner == dev_account, Error::<T>::OriginNotOwner);
785
786                    dapp_info.reward_beneficiary = beneficiary.clone();
787
788                    Ok(())
789                },
790            )?;
791
792            Self::deposit_event(Event::<T>::DAppRewardDestinationUpdated {
793                smart_contract,
794                beneficiary,
795            });
796
797            Ok(())
798        }
799
800        /// Used to change dApp owner.
801        ///
802        /// Can be called by dApp owner or dApp staking manager origin.
803        /// This is useful in two cases:
804        /// 1. when the dApp owner account is compromised, manager can change the owner to a new account
805        /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.).
806        #[pallet::call_index(3)]
807        #[pallet::weight(T::WeightInfo::set_dapp_owner())]
808        pub fn set_dapp_owner(
809            origin: OriginFor<T>,
810            smart_contract: T::SmartContract,
811            new_owner: T::AccountId,
812        ) -> DispatchResult {
813            Self::ensure_pallet_enabled()?;
814            let origin = ensure_signed_or_root(origin)?;
815
816            IntegratedDApps::<T>::try_mutate(
817                &smart_contract,
818                |maybe_dapp_info| -> DispatchResult {
819                    let dapp_info = maybe_dapp_info
820                        .as_mut()
821                        .ok_or(Error::<T>::ContractNotFound)?;
822
823                    // If manager origin, `None`, no need to check if caller is the owner.
824                    if let Some(caller) = origin {
825                        ensure!(dapp_info.owner == caller, Error::<T>::OriginNotOwner);
826                    }
827
828                    dapp_info.owner = new_owner.clone();
829
830                    Ok(())
831                },
832            )?;
833
834            Self::deposit_event(Event::<T>::DAppOwnerChanged {
835                smart_contract,
836                new_owner,
837            });
838
839            Ok(())
840        }
841
842        /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards.
843        /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking.
844        ///
845        /// Can be called by dApp staking manager origin.
846        #[pallet::call_index(6)]
847        #[pallet::weight(T::WeightInfo::unregister())]
848        pub fn unregister(
849            origin: OriginFor<T>,
850            smart_contract: T::SmartContract,
851        ) -> DispatchResult {
852            Self::ensure_pallet_enabled()?;
853            T::ContractUnregisterOrigin::ensure_origin(origin)?;
854
855            let dapp_info =
856                IntegratedDApps::<T>::get(&smart_contract).ok_or(Error::<T>::ContractNotFound)?;
857
858            ContractStake::<T>::remove(&dapp_info.id);
859            IntegratedDApps::<T>::remove(&smart_contract);
860
861            let current_era = ActiveProtocolState::<T>::get().era;
862            Self::deposit_event(Event::<T>::DAppUnregistered {
863                smart_contract,
864                era: current_era,
865            });
866
867            Ok(())
868        }
869
870        /// Locks additional funds into dApp staking.
871        ///
872        /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked.
873        /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount.
874        ///
875        /// Locked amount can immediately be used for staking.
876        #[pallet::call_index(7)]
877        #[pallet::weight(T::WeightInfo::lock_new_account().max(T::WeightInfo::lock_existing_account()))]
878        pub fn lock(
879            origin: OriginFor<T>,
880            #[pallet::compact] amount: Balance,
881        ) -> DispatchResultWithPostInfo {
882            Self::ensure_pallet_enabled()?;
883            let account = ensure_signed(origin)?;
884
885            let mut ledger = Ledger::<T>::get(&account);
886
887            // Only do the check for new accounts.
888            // External logic should ensure that accounts which are already participating in dApp staking aren't
889            // allowed to participate elsewhere where they shouldn't.
890            let is_new_account = ledger.is_empty();
891            if is_new_account {
892                ensure!(
893                    T::AccountCheck::allowed_to_stake(&account),
894                    Error::<T>::AccountNotAvailableForDappStaking
895                );
896            }
897
898            // Calculate & check amount available for locking
899            let available_balance =
900                T::Currency::total_balance(&account).saturating_sub(ledger.total_locked_amount());
901            let amount_to_lock = available_balance.min(amount);
902            ensure!(!amount_to_lock.is_zero(), Error::<T>::ZeroAmount);
903
904            ledger.add_lock_amount(amount_to_lock);
905
906            ensure!(
907                ledger.active_locked_amount() >= T::MinimumLockedAmount::get(),
908                Error::<T>::LockedAmountBelowThreshold
909            );
910
911            Self::update_ledger(&account, ledger)?;
912            CurrentEraInfo::<T>::mutate(|era_info| {
913                era_info.add_locked(amount_to_lock);
914            });
915
916            Self::deposit_event(Event::<T>::Locked {
917                account,
918                amount: amount_to_lock,
919            });
920
921            Ok(Some(if is_new_account {
922                T::WeightInfo::lock_new_account()
923            } else {
924                T::WeightInfo::lock_existing_account()
925            })
926            .into())
927        }
928
929        /// Attempts to start the unlocking process for the specified amount.
930        ///
931        /// Only the amount that isn't actively used for staking can be unlocked.
932        /// If the amount is greater than the available amount for unlocking, everything is unlocked.
933        /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked.
934        #[pallet::call_index(8)]
935        #[pallet::weight(T::WeightInfo::unlock())]
936        pub fn unlock(origin: OriginFor<T>, #[pallet::compact] amount: Balance) -> DispatchResult {
937            Self::ensure_pallet_enabled()?;
938            let account = ensure_signed(origin)?;
939
940            let state = ActiveProtocolState::<T>::get();
941            let mut ledger = Ledger::<T>::get(&account);
942
943            let available_for_unlocking = ledger.unlockable_amount(state.period_info.number);
944            let amount_to_unlock = available_for_unlocking.min(amount);
945
946            // Ensure we unlock everything if remaining amount is below threshold.
947            let remaining_amount = ledger
948                .active_locked_amount()
949                .saturating_sub(amount_to_unlock);
950            let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() {
951                ensure!(
952                    ledger.staked_amount(state.period_info.number).is_zero(),
953                    Error::<T>::RemainingStakePreventsFullUnlock
954                );
955                ledger.active_locked_amount()
956            } else {
957                amount_to_unlock
958            };
959
960            // Sanity check
961            ensure!(!amount_to_unlock.is_zero(), Error::<T>::ZeroAmount);
962
963            // Update ledger with new lock and unlocking amounts
964            ledger.subtract_lock_amount(amount_to_unlock);
965
966            let current_block = frame_system::Pallet::<T>::block_number();
967            let unlock_block = current_block.saturating_add(Self::unlocking_period().into());
968            ledger
969                .add_unlocking_chunk(amount_to_unlock, unlock_block.saturated_into())
970                .map_err(|_| Error::<T>::TooManyUnlockingChunks)?;
971
972            // Update storage
973            Self::update_ledger(&account, ledger)?;
974            CurrentEraInfo::<T>::mutate(|era_info| {
975                era_info.unlocking_started(amount_to_unlock);
976            });
977
978            Self::deposit_event(Event::<T>::Unlocking {
979                account,
980                amount: amount_to_unlock,
981            });
982
983            Ok(())
984        }
985
986        /// Claims all of fully unlocked chunks, removing the lock from them.
987        #[pallet::call_index(9)]
988        #[pallet::weight(T::WeightInfo::claim_unlocked(T::MaxNumberOfStakedContracts::get()))]
989        pub fn claim_unlocked(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
990            Self::ensure_pallet_enabled()?;
991            let account = ensure_signed(origin)?;
992
993            Self::internal_claim_unlocked(account)
994        }
995
996        #[pallet::call_index(10)]
997        #[pallet::weight(T::WeightInfo::relock_unlocking())]
998        pub fn relock_unlocking(origin: OriginFor<T>) -> DispatchResult {
999            Self::ensure_pallet_enabled()?;
1000            let account = ensure_signed(origin)?;
1001
1002            let mut ledger = Ledger::<T>::get(&account);
1003
1004            ensure!(!ledger.unlocking.is_empty(), Error::<T>::NoUnlockingChunks);
1005
1006            let amount = ledger.consume_unlocking_chunks();
1007
1008            ledger.add_lock_amount(amount);
1009            ensure!(
1010                ledger.active_locked_amount() >= T::MinimumLockedAmount::get(),
1011                Error::<T>::LockedAmountBelowThreshold
1012            );
1013
1014            Self::update_ledger(&account, ledger)?;
1015            CurrentEraInfo::<T>::mutate(|era_info| {
1016                era_info.add_locked(amount);
1017                era_info.unlocking_removed(amount);
1018            });
1019
1020            Self::deposit_event(Event::<T>::Relock { account, amount });
1021
1022            Ok(())
1023        }
1024
1025        /// Stake the specified amount on a smart contract.
1026        /// The precise `amount` specified **must** be available for staking.
1027        /// The total amount staked on a dApp must be greater than the minimum required value.
1028        ///
1029        /// Depending on the period type, appropriate stake amount will be updated. During `Voting` subperiod, `voting` stake amount is updated,
1030        /// and same for `Build&Earn` subperiod.
1031        ///
1032        /// Staked amount is only eligible for rewards from the next era onwards.
1033        #[pallet::call_index(11)]
1034        #[pallet::weight(T::WeightInfo::stake())]
1035        pub fn stake(
1036            origin: OriginFor<T>,
1037            smart_contract: T::SmartContract,
1038            #[pallet::compact] amount: Balance,
1039        ) -> DispatchResult {
1040            Self::ensure_pallet_enabled()?;
1041            let account = ensure_signed(origin)?;
1042
1043            // User is only eligible for the bonus reward if their first time stake is in the `Voting` subperiod.
1044            //
1045            // `StakeAmount` is prepared based on the current subperiod.
1046            // If the user is staking for the first time in the `Voting` subperiod, they are eligible for the bonus reward, and the max number of bonus moves is set.
1047            // If the user is staking for the first time in the `Build&Earn` subperiod, they are not eligible for the bonus reward, and the bonus moves are set to 0.
1048            let protocol_state = ActiveProtocolState::<T>::get();
1049            let (stake_amount, bonus_status) = match protocol_state.subperiod() {
1050                Subperiod::Voting => (
1051                    StakeAmount {
1052                        voting: amount,
1053                        build_and_earn: 0,
1054                        era: protocol_state.era,
1055                        period: protocol_state.period_number(),
1056                    },
1057                    *BonusStatusWrapperFor::<T>::default(),
1058                ),
1059                Subperiod::BuildAndEarn => (
1060                    StakeAmount {
1061                        voting: 0,
1062                        build_and_earn: amount,
1063                        era: protocol_state.era,
1064                        period: protocol_state.period_number(),
1065                    },
1066                    0,
1067                ),
1068            };
1069
1070            // The `inner_stake` function takes a `StakeAmount` struct allowing modification of both `voting` and `build_and_earn` amounts at the same time.
1071            Self::inner_stake(&account, &smart_contract, stake_amount, bonus_status)?;
1072
1073            Self::deposit_event(Event::<T>::Stake {
1074                account,
1075                smart_contract,
1076                amount,
1077            });
1078
1079            Ok(())
1080        }
1081
1082        /// Unstake the specified amount from a smart contract.
1083        /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail.
1084        ///
1085        /// If unstaking the specified `amount` would take staker below the minimum stake threshold, everything is unstaked.
1086        ///
1087        /// Depending on the period type, appropriate stake amount will be updated.
1088        /// In case amount is unstaked during `Voting` subperiod, the `voting` amount is reduced.
1089        /// In case amount is unstaked during `Build&Earn` subperiod, first the `build_and_earn` is reduced,
1090        /// and any spillover is subtracted from the `voting` amount.
1091        #[pallet::call_index(12)]
1092        #[pallet::weight(T::WeightInfo::unstake())]
1093        pub fn unstake(
1094            origin: OriginFor<T>,
1095            smart_contract: T::SmartContract,
1096            #[pallet::compact] amount: Balance,
1097        ) -> DispatchResult {
1098            Self::ensure_pallet_enabled()?;
1099            let account = ensure_signed(origin)?;
1100
1101            let (unstake_amount, _) = Self::inner_unstake(&account, &smart_contract, amount)?;
1102
1103            Self::deposit_event(Event::<T>::Unstake {
1104                account,
1105                smart_contract,
1106                amount: unstake_amount.total(),
1107            });
1108
1109            Ok(())
1110        }
1111
1112        /// Claims some staker rewards, if user has any.
1113        /// In the case of a successful call, at least one era will be claimed, with the possibility of multiple claims happening.
1114        #[pallet::call_index(13)]
1115        #[pallet::weight({
1116            let max_span_length = T::EraRewardSpanLength::get();
1117            T::WeightInfo::claim_staker_rewards_ongoing_period(max_span_length)
1118                .max(T::WeightInfo::claim_staker_rewards_past_period(max_span_length))
1119        })]
1120        pub fn claim_staker_rewards(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
1121            Self::ensure_pallet_enabled()?;
1122            let account = ensure_signed(origin)?;
1123
1124            Self::internal_claim_staker_rewards_for(account)
1125        }
1126
1127        /// Used to claim bonus reward for a smart contract, if eligible.
1128        #[pallet::call_index(14)]
1129        #[pallet::weight(T::WeightInfo::claim_bonus_reward())]
1130        pub fn claim_bonus_reward(
1131            origin: OriginFor<T>,
1132            smart_contract: T::SmartContract,
1133        ) -> DispatchResult {
1134            Self::ensure_pallet_enabled()?;
1135            let account = ensure_signed(origin)?;
1136
1137            Self::internal_claim_bonus_reward_for(account, smart_contract)
1138        }
1139
1140        /// Used to claim dApp reward for the specified era.
1141        #[pallet::call_index(15)]
1142        #[pallet::weight(T::WeightInfo::claim_dapp_reward())]
1143        pub fn claim_dapp_reward(
1144            origin: OriginFor<T>,
1145            smart_contract: T::SmartContract,
1146            #[pallet::compact] era: EraNumber,
1147        ) -> DispatchResult {
1148            Self::ensure_pallet_enabled()?;
1149
1150            // To keep in line with legacy behavior, dApp rewards can be claimed by anyone.
1151            let _ = ensure_signed(origin)?;
1152
1153            let dapp_info =
1154                IntegratedDApps::<T>::get(&smart_contract).ok_or(Error::<T>::ContractNotFound)?;
1155
1156            // Make sure provided era has ended
1157            let protocol_state = ActiveProtocolState::<T>::get();
1158            ensure!(era < protocol_state.era, Error::<T>::InvalidClaimEra);
1159
1160            // 'Consume' dApp reward for the specified era, if possible.
1161            let mut dapp_tiers = DAppTiers::<T>::get(&era).ok_or(Error::<T>::NoDAppTierInfo)?;
1162            ensure!(
1163                dapp_tiers.period >= Self::oldest_claimable_period(protocol_state.period_number()),
1164                Error::<T>::RewardExpired
1165            );
1166
1167            let (amount, ranked_tier) =
1168                dapp_tiers
1169                    .try_claim(dapp_info.id)
1170                    .map_err(|error| match error {
1171                        DAppTierError::NoDAppInTiers => Error::<T>::NoClaimableRewards,
1172                        _ => Error::<T>::InternalClaimDAppError,
1173                    })?;
1174
1175            let (tier_id, rank) = ranked_tier.deconstruct();
1176
1177            // Get reward destination, and deposit the reward.
1178            let beneficiary = dapp_info.reward_beneficiary();
1179            T::StakingRewardHandler::payout_reward(&beneficiary, amount)
1180                .map_err(|_| Error::<T>::RewardPayoutFailed)?;
1181
1182            // Write back updated struct to prevent double reward claims
1183            DAppTiers::<T>::insert(&era, dapp_tiers);
1184
1185            Self::deposit_event(Event::<T>::DAppReward {
1186                beneficiary: beneficiary.clone(),
1187                smart_contract,
1188                tier_id,
1189                rank,
1190                era,
1191                amount,
1192            });
1193
1194            Ok(())
1195        }
1196
1197        /// Used to unstake funds from a contract that was unregistered after an account staked on it.
1198        /// This is required if staker wants to re-stake these funds on another active contract during the ongoing period.
1199        #[pallet::call_index(16)]
1200        #[pallet::weight(T::WeightInfo::unstake_from_unregistered())]
1201        pub fn unstake_from_unregistered(
1202            origin: OriginFor<T>,
1203            smart_contract: T::SmartContract,
1204        ) -> DispatchResult {
1205            Self::ensure_pallet_enabled()?;
1206            let account = ensure_signed(origin)?;
1207
1208            let (unstake_amount, _) =
1209                Self::inner_unstake_from_unregistered(&account, &smart_contract)?;
1210
1211            Self::deposit_event(Event::<T>::UnstakeFromUnregistered {
1212                account,
1213                smart_contract,
1214                amount: unstake_amount.total(),
1215            });
1216
1217            Ok(())
1218        }
1219
1220        /// Cleanup expired stake entries for the contract.
1221        ///
1222        /// Entry is considered to be expired if:
1223        /// 1. It's from a past period & the account did not maintain an eligible bonus status, meaning there's no claimable bonus reward.
1224        /// 2. It's from a period older than the oldest claimable period, regardless of whether the account had an eligible bonus status or not.
1225        #[pallet::call_index(17)]
1226        #[pallet::weight(T::WeightInfo::cleanup_expired_entries(
1227            T::MaxNumberOfStakedContracts::get()
1228        ))]
1229        pub fn cleanup_expired_entries(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
1230            Self::ensure_pallet_enabled()?;
1231            let account = ensure_signed(origin)?;
1232
1233            let protocol_state = ActiveProtocolState::<T>::get();
1234            let current_period = protocol_state.period_number();
1235            let threshold_period = Self::oldest_claimable_period(current_period);
1236
1237            let mut remaining: u32 = 0;
1238            let mut to_be_deleted: Vec<T::SmartContract> = Vec::new();
1239
1240            // Partition stake entries into remaining (valid) and to-be-deleted (expired).
1241            // An entry is expired if it's from a past period without bonus eligibility,
1242            // or older than the oldest claimable period regardless of bonus status.
1243            // Bounded by max allowed number of stake entries per account.
1244            for (smart_contract, stake_info) in StakerInfo::<T>::iter_prefix(&account) {
1245                let stake_period = stake_info.period_number();
1246
1247                // Check if this entry should be kept
1248                let should_keep = stake_period == current_period
1249                    || (stake_period >= threshold_period
1250                        && stake_period < current_period
1251                        && stake_info.is_bonus_eligible());
1252
1253                if should_keep {
1254                    remaining = remaining.saturating_add(1);
1255                } else {
1256                    to_be_deleted.push(smart_contract);
1257                }
1258            }
1259            let entries_to_delete = to_be_deleted.len();
1260
1261            ensure!(!entries_to_delete.is_zero(), Error::<T>::NoExpiredEntries);
1262
1263            // Remove all expired entries.
1264            for smart_contract in to_be_deleted {
1265                StakerInfo::<T>::remove(&account, &smart_contract);
1266            }
1267
1268            // Remove expired stake entries from the ledger.
1269            let mut ledger = Ledger::<T>::get(&account);
1270            ledger.contract_stake_count = remaining;
1271            ledger.maybe_cleanup_expired(threshold_period); // Not necessary but we do it for the sake of consistency
1272            Self::update_ledger(&account, ledger)?;
1273
1274            Self::deposit_event(Event::<T>::ExpiredEntriesRemoved {
1275                account,
1276                count: entries_to_delete.unique_saturated_into(),
1277            });
1278
1279            Ok(Some(T::WeightInfo::cleanup_expired_entries(
1280                entries_to_delete.unique_saturated_into(),
1281            ))
1282            .into())
1283        }
1284
1285        /// Used to force a change of era or subperiod.
1286        /// The effect isn't immediate but will happen on the next block.
1287        ///
1288        /// Used for testing purposes, when we want to force an era change, or a subperiod change.
1289        /// Not intended to be used in production, except in case of unforeseen circumstances.
1290        ///
1291        /// Can only be called by the root origin.
1292        #[pallet::call_index(18)]
1293        #[pallet::weight(T::WeightInfo::force())]
1294        pub fn force(origin: OriginFor<T>, forcing_type: ForcingType) -> DispatchResult {
1295            Self::ensure_pallet_enabled()?;
1296            ensure_root(origin)?;
1297
1298            ensure!(!Safeguard::<T>::get(), Error::<T>::ForceNotAllowed);
1299
1300            // Ensure a 'change' happens on the next block
1301            ActiveProtocolState::<T>::mutate(|state| {
1302                let current_block = frame_system::Pallet::<T>::block_number();
1303                state.next_era_start = current_block.saturating_add(One::one()).saturated_into();
1304
1305                match forcing_type {
1306                    ForcingType::Era => (),
1307                    ForcingType::Subperiod => {
1308                        state.period_info.next_subperiod_start_era = state.era.saturating_add(1);
1309                    }
1310                }
1311
1312                //       Right now it won't account for the full weight incurred by calling this notification.
1313                //       It's not a big problem since this call is not expected to be called ever in production.
1314                //       Also, in case of subperiod forcing, the alignment will be broken but since this is only call for testing,
1315                //       we don't need to concern ourselves with it.
1316                Self::notify_block_before_new_era(&state);
1317            });
1318
1319            Self::deposit_event(Event::<T>::Force { forcing_type });
1320
1321            Ok(())
1322        }
1323
1324        /// Claims some staker rewards for the specified account, if they have any.
1325        /// In the case of a successful call, at least one era will be claimed, with the possibility of multiple claims happening.
1326        #[pallet::call_index(19)]
1327        #[pallet::weight({
1328            let max_span_length = T::EraRewardSpanLength::get();
1329            T::WeightInfo::claim_staker_rewards_ongoing_period(max_span_length)
1330                .max(T::WeightInfo::claim_staker_rewards_past_period(max_span_length))
1331        })]
1332        pub fn claim_staker_rewards_for(
1333            origin: OriginFor<T>,
1334            account: T::AccountId,
1335        ) -> DispatchResultWithPostInfo {
1336            Self::ensure_pallet_enabled()?;
1337            ensure_signed(origin)?;
1338
1339            Self::internal_claim_staker_rewards_for(account)
1340        }
1341
1342        /// Used to claim bonus reward for a smart contract on behalf of the specified account, if eligible.
1343        #[pallet::call_index(20)]
1344        #[pallet::weight(T::WeightInfo::claim_bonus_reward())]
1345        pub fn claim_bonus_reward_for(
1346            origin: OriginFor<T>,
1347            account: T::AccountId,
1348            smart_contract: T::SmartContract,
1349        ) -> DispatchResult {
1350            Self::ensure_pallet_enabled()?;
1351            ensure_signed(origin)?;
1352
1353            Self::internal_claim_bonus_reward_for(account, smart_contract)
1354        }
1355
1356        /// Transfers stake between two smart contracts, ensuring bonus status preservation if eligible.
1357        /// Emits a `StakeMoved` event.
1358        #[pallet::call_index(21)]
1359        #[pallet::weight(T::WeightInfo::move_stake_unregistered_source().max(T::WeightInfo::move_stake_from_registered_source()))]
1360        pub fn move_stake(
1361            origin: OriginFor<T>,
1362            source_contract: T::SmartContract,
1363            destination_contract: T::SmartContract,
1364            #[pallet::compact] amount: Balance,
1365        ) -> DispatchResultWithPostInfo {
1366            Self::ensure_pallet_enabled()?;
1367            let account = ensure_signed(origin)?;
1368
1369            ensure!(
1370                !source_contract.eq(&destination_contract),
1371                Error::<T>::SameContracts
1372            );
1373
1374            ensure!(
1375                IntegratedDApps::<T>::contains_key(&destination_contract),
1376                Error::<T>::ContractNotFound
1377            );
1378
1379            let maybe_source_dapp_info = IntegratedDApps::<T>::get(&source_contract);
1380            let is_source_unregistered = maybe_source_dapp_info.is_none();
1381
1382            let (mut move_amount, bonus_status) = if is_source_unregistered {
1383                Self::inner_unstake_from_unregistered(&account, &source_contract)?
1384            } else {
1385                Self::inner_unstake(&account, &source_contract, amount)?
1386            };
1387
1388            // When bonus is forfeited, voting stake must be merged into b&e stake
1389            if bonus_status == 0 && move_amount.voting > 0 {
1390                move_amount.convert_bonus_into_regular_stake();
1391            }
1392
1393            Self::inner_stake(&account, &destination_contract, move_amount, bonus_status)?;
1394
1395            Self::deposit_event(Event::<T>::StakeMoved {
1396                account,
1397                source_contract,
1398                destination_contract,
1399                amount: move_amount.total(),
1400            });
1401
1402            Ok(Some(if is_source_unregistered {
1403                T::WeightInfo::move_stake_unregistered_source()
1404            } else {
1405                T::WeightInfo::move_stake_from_registered_source()
1406            })
1407            .into())
1408        }
1409
1410        /// Used to set static tier parameters, which are used to calculate tier configuration.
1411        /// Tier configuration defines tier entry threshold values, number of slots, and reward portions.
1412        ///
1413        /// This is a delicate call and great care should be taken when changing these
1414        /// values since it has a significant impact on the reward system.
1415        #[pallet::call_index(22)]
1416        #[pallet::weight(T::WeightInfo::set_static_tier_params())]
1417        pub fn set_static_tier_params(
1418            origin: OriginFor<T>,
1419            params: TierParameters<T::NumberOfTiers>,
1420        ) -> DispatchResult {
1421            Self::ensure_pallet_enabled()?;
1422            ensure_root(origin)?;
1423            ensure!(params.is_valid(), Error::<T>::InvalidTierParams);
1424
1425            StaticTierParams::<T>::set(params.clone());
1426
1427            Self::deposit_event(Event::<T>::NewTierParameters { params });
1428
1429            Ok(())
1430        }
1431    }
1432
1433    impl<T: Config> Pallet<T> {
1434        /// Inner `unstake` functionality for an **active** smart contract.
1435        /// If successful returns the `StakeAmount` that was unstaked, and the updated bonus status.
1436        ///
1437        /// - Ensures the contract is still registered.
1438        /// - Updates staker info, ledger, and contract stake info.
1439        /// - Returns the unstaked amount and updated bonus status.
1440        pub fn inner_unstake(
1441            account: &T::AccountId,
1442            smart_contract: &T::SmartContract,
1443            amount: Balance,
1444        ) -> Result<(StakeAmount, BonusStatus), DispatchError> {
1445            ensure!(amount > 0, Error::<T>::ZeroAmount);
1446            let dapp_info =
1447                IntegratedDApps::<T>::get(&smart_contract).ok_or(Error::<T>::ContractNotFound)?;
1448
1449            let protocol_state = ActiveProtocolState::<T>::get();
1450            let current_era = protocol_state.era;
1451
1452            let mut ledger = Ledger::<T>::get(&account);
1453
1454            // 1.
1455            // Update `StakerInfo` storage with the reduced stake amount on the specified contract.
1456            let (new_staking_info, amount, stake_amount_iter, updated_bonus_status) =
1457                match StakerInfo::<T>::get(&account, &smart_contract) {
1458                    Some(mut staking_info) => {
1459                        ensure!(
1460                            staking_info.period_number() == protocol_state.period_number(),
1461                            Error::<T>::UnstakeFromPastPeriod
1462                        );
1463                        ensure!(
1464                            staking_info.total_staked_amount() >= amount,
1465                            Error::<T>::UnstakeAmountTooLarge
1466                        );
1467
1468                        // If unstaking would take the total staked amount below the minimum required value,
1469                        // unstake everything.
1470                        let amount = if staking_info.total_staked_amount().saturating_sub(amount)
1471                            < T::MinimumStakeAmount::get()
1472                        {
1473                            staking_info.total_staked_amount()
1474                        } else {
1475                            amount
1476                        };
1477
1478                        let (stake_amount_iter, updated_bonus_status) =
1479                            staking_info.unstake(amount, current_era, protocol_state.subperiod());
1480
1481                        (
1482                            staking_info,
1483                            amount,
1484                            stake_amount_iter,
1485                            updated_bonus_status,
1486                        )
1487                    }
1488                    None => {
1489                        return Err(Error::<T>::NoStakingInfo.into());
1490                    }
1491                };
1492
1493            // 2.
1494            // Reduce stake amount
1495            ledger
1496                .unstake_amount(amount, current_era, protocol_state.period_info)
1497                .map_err(|err| match err {
1498                    AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => {
1499                        Error::<T>::UnclaimedRewards
1500                    }
1501                    // This is a defensive check, which should never happen since we calculate the correct value above.
1502                    AccountLedgerError::UnstakeAmountLargerThanStake => {
1503                        Error::<T>::UnstakeAmountTooLarge
1504                    }
1505                    _ => Error::<T>::InternalUnstakeError,
1506                })?;
1507
1508            // 3.
1509            // Update `ContractStake` storage with the reduced stake amount on the specified contract.
1510            let mut contract_stake_info = ContractStake::<T>::get(&dapp_info.id);
1511            contract_stake_info.unstake(
1512                &stake_amount_iter,
1513                protocol_state.period_info,
1514                current_era,
1515            );
1516
1517            // 4.
1518            // Update total staked amount for the next era.
1519            CurrentEraInfo::<T>::mutate(|era_info| {
1520                era_info.unstake_amount(stake_amount_iter.clone());
1521            });
1522
1523            // 5.
1524            // Update remaining storage entries
1525            ContractStake::<T>::insert(&dapp_info.id, contract_stake_info);
1526
1527            if new_staking_info.is_empty() {
1528                ledger.contract_stake_count.saturating_dec();
1529                StakerInfo::<T>::remove(&account, &smart_contract);
1530            } else {
1531                StakerInfo::<T>::insert(&account, &smart_contract, new_staking_info);
1532            }
1533
1534            Self::update_ledger(&account, ledger)?;
1535
1536            // Return the `StakeAmount` that has max total value.
1537            let mut unstake_amount = stake_amount_iter
1538                .iter()
1539                .max_by(|a, b| a.total().cmp(&b.total()))
1540                // At least one value exists, otherwise we wouldn't be here.
1541                .ok_or(Error::<T>::InternalUnstakeError)?
1542                .clone();
1543
1544            // Ensure we use the current era instead of potentially next era
1545            unstake_amount.era = current_era;
1546
1547            Ok((unstake_amount, updated_bonus_status))
1548        }
1549
1550        /// Handles unstaking from an **unregistered** smart contract.
1551        ///
1552        /// - Ensures the contract is no longer active.
1553        /// - Updates staker info and ledger.
1554        /// - Returns the unstaked amount and preserves the original bonus status.
1555        pub fn inner_unstake_from_unregistered(
1556            account: &T::AccountId,
1557            smart_contract: &T::SmartContract,
1558        ) -> Result<(StakeAmount, BonusStatus), DispatchError> {
1559            ensure!(
1560                !IntegratedDApps::<T>::contains_key(&smart_contract),
1561                Error::<T>::ContractStillActive
1562            );
1563
1564            let protocol_state = ActiveProtocolState::<T>::get();
1565            let current_era = protocol_state.era;
1566
1567            // Extract total staked amount on the specified unregistered contract
1568            let (amount, unstake_amount_iter, preserved_bonus_status) =
1569                match StakerInfo::<T>::get(&account, &smart_contract) {
1570                    Some(mut staking_info) => {
1571                        ensure!(
1572                            staking_info.period_number() == protocol_state.period_number(),
1573                            Error::<T>::UnstakeFromPastPeriod
1574                        );
1575
1576                        let preserved_bonus_status = staking_info.bonus_status;
1577                        let amount = staking_info.staked.total();
1578
1579                        let (unstake_amount_iter, _) =
1580                            staking_info.unstake(amount, current_era, protocol_state.subperiod());
1581
1582                        (amount, unstake_amount_iter, preserved_bonus_status)
1583                    }
1584                    None => {
1585                        return Err(Error::<T>::NoStakingInfo.into());
1586                    }
1587                };
1588
1589            // Reduce stake amount in ledger
1590            let mut ledger = Ledger::<T>::get(&account);
1591            ledger
1592                .unstake_amount(amount, current_era, protocol_state.period_info)
1593                .map_err(|err| match err {
1594                    // These are all defensive checks, which should never fail since we already checked them above.
1595                    AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => {
1596                        Error::<T>::UnclaimedRewards
1597                    }
1598                    _ => Error::<T>::InternalUnstakeError,
1599                })?;
1600            ledger.contract_stake_count.saturating_dec();
1601
1602            // Update total staked amount for the next era.
1603            // This means 'fake' stake total amount has been kept until now, even though contract was unregistered.
1604            // Although strange, it's been requested to keep it like this from the team.
1605            CurrentEraInfo::<T>::mutate(|era_info| {
1606                era_info.unstake_amount(unstake_amount_iter.clone());
1607            });
1608
1609            // Update remaining storage entries
1610            Self::update_ledger(&account, ledger)?;
1611            StakerInfo::<T>::remove(&account, &smart_contract);
1612
1613            // Return the `StakeAmount` that has max total value.
1614            let mut unstake_amount = unstake_amount_iter
1615                .iter()
1616                .max_by(|a, b| a.total().cmp(&b.total()))
1617                // At least one value exists, otherwise we wouldn't be here.
1618                .ok_or(Error::<T>::InternalUnstakeError)?
1619                .clone();
1620
1621            // Ensure we use the current era instead of potentially next era
1622            unstake_amount.era = current_era;
1623
1624            Ok((unstake_amount, preserved_bonus_status))
1625        }
1626
1627        /// Inner `stake` functionality.
1628        ///
1629        /// Specifies the amount in the form of the `StakeAmount` struct, allowing simultaneous update of both `voting` and `build_and_earn` amounts.
1630        /// The `bonus_status` is used to determine if the staker is still eligible for the bonus reward. This is useful for the `move` extrinsic.
1631        pub fn inner_stake(
1632            account: &T::AccountId,
1633            smart_contract: &T::SmartContract,
1634            amount: StakeAmount,
1635            bonus_status: BonusStatus,
1636        ) -> Result<(), DispatchError> {
1637            ensure!(amount.total() > 0, Error::<T>::ZeroAmount);
1638
1639            let dapp_info =
1640                IntegratedDApps::<T>::get(&smart_contract).ok_or(Error::<T>::ContractNotFound)?;
1641
1642            let protocol_state = ActiveProtocolState::<T>::get();
1643            let current_era = protocol_state.era;
1644            let period_number = protocol_state.period_info.number;
1645            ensure!(
1646                !protocol_state
1647                    .period_info
1648                    .is_next_period(current_era.saturating_add(1)),
1649                Error::<T>::PeriodEndsInNextEra
1650            );
1651
1652            let mut ledger = Ledger::<T>::get(&account);
1653
1654            // In case old stake rewards are unclaimed & have expired, clean them up.
1655            let threshold_period = Self::oldest_claimable_period(protocol_state.period_number());
1656            let _ignore = ledger.maybe_cleanup_expired(threshold_period);
1657
1658            // 1.
1659            // Increase stake amount for the next era & current period in staker's ledger
1660            ledger
1661                .add_stake_amount(amount, current_era, protocol_state.period_info)
1662                .map_err(|err| match err {
1663                    AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => {
1664                        Error::<T>::UnclaimedRewards
1665                    }
1666                    AccountLedgerError::UnavailableStakeFunds => Error::<T>::UnavailableStakeFunds,
1667                    // Defensive check, should never happen
1668                    _ => Error::<T>::InternalStakeError,
1669                })?;
1670
1671            // 2.
1672            // Update `StakerInfo` storage with the new stake amount on the specified contract.
1673            //
1674            // There are three distinct scenarios:
1675            // 1. Existing entry matches the current period number - just update it.
1676            // 2. Existing entry is expired/orphaned - replace it.
1677            // 3. Entry doesn't exist or it's for an older period - create a new one.
1678            //
1679            // This is ok since we only use this storage entry to keep track of how much each staker
1680            // has staked on each contract in the current period. We only ever need the latest information.
1681            // This is because `AccountLedger` is the one keeping information about how much was staked when.
1682            let (mut new_staking_info, is_new_entry, replacing_old_entry) =
1683                match StakerInfo::<T>::get(&account, &smart_contract) {
1684                    // Entry with matching period exists
1685                    Some(staking_info)
1686                        if staking_info.period_number() == protocol_state.period_number() =>
1687                    {
1688                        (staking_info, false, false)
1689                    }
1690                    // Entry exists but period doesn't match. Bonus reward might still be claimable.
1691                    Some(staking_info)
1692                        if staking_info.period_number() >= threshold_period
1693                            && staking_info.is_bonus_eligible() =>
1694                    {
1695                        return Err(Error::<T>::UnclaimedRewards.into());
1696                    }
1697                    // Entry exists but is expired/orphaned - we're replacing it, not adding new
1698                    Some(_old_entry) => {
1699                        // Remove the old orphaned entry explicitly
1700                        StakerInfo::<T>::remove(&account, &smart_contract);
1701                        (
1702                            SingularStakingInfo::new(protocol_state.period_number(), bonus_status),
1703                            true, // is_new_entry (for storage write)
1704                            true, // replacing_old_entry (don't increment counter)
1705                        )
1706                    }
1707                    // No entry exists at all - truly new
1708                    None => (
1709                        SingularStakingInfo::new(
1710                            protocol_state.period_number(),
1711                            // Important to account for the remaining bonus moves since it's possible to retain the bonus
1712                            // after moving the stake to another contract.
1713                            bonus_status,
1714                        ),
1715                        true,
1716                        false,
1717                    ),
1718                };
1719
1720            new_staking_info.stake(amount, current_era, bonus_status);
1721            ensure!(
1722                new_staking_info.total_staked_amount() >= T::MinimumStakeAmount::get(),
1723                Error::<T>::InsufficientStakeAmount
1724            );
1725
1726            // Only increment if we're truly adding a new contract, not replacing an orphaned entry
1727            if is_new_entry && !replacing_old_entry {
1728                ledger.contract_stake_count.saturating_inc();
1729                ensure!(
1730                    ledger.contract_stake_count <= T::MaxNumberOfStakedContracts::get(),
1731                    Error::<T>::TooManyStakedContracts
1732                );
1733            }
1734
1735            // 3.
1736            // Update `ContractStake` storage with the new stake amount on the specified contract.
1737            let mut contract_stake_info = ContractStake::<T>::get(&dapp_info.id);
1738            contract_stake_info.stake(amount, current_era, period_number);
1739
1740            // 4.
1741            // Update total staked amount for the next era.
1742            CurrentEraInfo::<T>::mutate(|era_info| {
1743                era_info.add_stake_amount(amount);
1744            });
1745
1746            // 5.
1747            // Update remaining storage entries
1748            Self::update_ledger(&account, ledger)?;
1749            StakerInfo::<T>::insert(&account, &smart_contract, new_staking_info);
1750            ContractStake::<T>::insert(&dapp_info.id, contract_stake_info);
1751
1752            Ok(())
1753        }
1754
1755        /// `true` if the account is a staker, `false` otherwise.
1756        pub fn is_staker(account: &T::AccountId) -> bool {
1757            Ledger::<T>::contains_key(account)
1758        }
1759
1760        /// `Err` if pallet disabled for maintenance, `Ok` otherwise.
1761        pub(crate) fn ensure_pallet_enabled() -> Result<(), Error<T>> {
1762            if ActiveProtocolState::<T>::get().maintenance {
1763                Err(Error::<T>::Disabled)
1764            } else {
1765                Ok(())
1766            }
1767        }
1768
1769        /// Update the account ledger, and dApp staking balance freeze.
1770        ///
1771        /// In case account ledger is empty, entries from the DB are removed and freeze is thawed.
1772        ///
1773        /// This call can fail if the `freeze` or `thaw` operations fail. This should never happen since
1774        /// runtime definition must ensure it supports necessary freezes.
1775        pub(crate) fn update_ledger(
1776            account: &T::AccountId,
1777            ledger: AccountLedgerFor<T>,
1778        ) -> Result<(), DispatchError> {
1779            if ledger.is_empty() {
1780                Ledger::<T>::remove(&account);
1781                T::Currency::thaw(&FreezeReason::DAppStaking.into(), account)?;
1782            } else {
1783                T::Currency::set_freeze(
1784                    &FreezeReason::DAppStaking.into(),
1785                    account,
1786                    ledger.total_locked_amount(),
1787                )?;
1788                Ledger::<T>::insert(account, ledger);
1789            }
1790
1791            Ok(())
1792        }
1793
1794        /// Returns the number of blocks per voting period.
1795        pub(crate) fn blocks_per_voting_period() -> BlockNumber {
1796            T::CycleConfiguration::blocks_per_era()
1797                .saturating_mul(T::CycleConfiguration::eras_per_voting_subperiod().into())
1798        }
1799
1800        /// Calculates the `EraRewardSpan` index for the specified era.
1801        pub fn era_reward_span_index(era: EraNumber) -> EraNumber {
1802            era.saturating_sub(era % T::EraRewardSpanLength::get())
1803        }
1804
1805        /// Return the oldest period for which rewards can be claimed.
1806        /// All rewards before that period are considered to be expired.
1807        pub(crate) fn oldest_claimable_period(current_period: PeriodNumber) -> PeriodNumber {
1808            current_period.saturating_sub(T::RewardRetentionInPeriods::get())
1809        }
1810
1811        /// Unlocking period expressed in the number of blocks.
1812        pub fn unlocking_period() -> BlockNumber {
1813            T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into())
1814        }
1815
1816        /// Returns the dApp tier assignment for the current era, based on the current stake amounts.
1817        pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier> {
1818            let protocol_state = ActiveProtocolState::<T>::get();
1819
1820            let (dapp_tiers, _count) = Self::get_dapp_tier_assignment_and_rewards(
1821                protocol_state.era,
1822                protocol_state.period_number(),
1823                Balance::zero(),
1824            );
1825
1826            dapp_tiers.dapps.into_inner()
1827        }
1828
1829        /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier.
1830        ///
1831        /// ### Algorithm
1832        ///
1833        /// 1. Read in over all contract stake entries. In case staked amount is zero for the current era, ignore it.
1834        ///    This information is used to calculate 'score' per dApp, which is used to determine the tier.
1835        ///
1836        /// 2. Sort the entries by the score, in descending order - the top score dApp comes first.
1837        ///
1838        /// 3. Calculate rewards for each tier.
1839        ///    This is done by dividing the total reward pool into tier reward pools,
1840        ///    after which the tier reward pool is divided by the number of available slots in the tier.
1841        ///
1842        /// 4. Read in tier configuration. This contains information about how many slots per tier there are,
1843        ///    as well as the threshold for each tier. Threshold is the minimum amount of stake required to be eligible for a tier.
1844        ///    Iterate over tier thresholds & capacities, starting from the top tier, and assign dApps to them.
1845        ///
1846        ///    ```text
1847        ////   for each tier:
1848        ///        for each unassigned dApp:
1849        ///            if tier has capacity && dApp satisfies the tier threshold:
1850        ///                add dapp to the tier
1851        ///            else:
1852        ///               exit loop since no more dApps will satisfy the threshold since they are sorted by score
1853        ///    ```
1854        ///    (Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search.)
1855        ///
1856        /// The returned object contains information about each dApp that made it into a tier.
1857        /// Alongside tier assignment info, number of read DB contract stake entries is returned.
1858        pub(crate) fn get_dapp_tier_assignment_and_rewards(
1859            era: EraNumber,
1860            period: PeriodNumber,
1861            dapp_reward_pool: Balance,
1862        ) -> (DAppTierRewardsFor<T>, DAppId) {
1863            let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts::get() as usize);
1864
1865            // 1.
1866            // Iterate over all staked dApps.
1867            // This is bounded by max amount of dApps we allow to be registered.
1868            let mut counter = 0;
1869            for (dapp_id, stake_amount) in ContractStake::<T>::iter() {
1870                counter.saturating_inc();
1871
1872                // Skip dApps which don't have ANY amount staked
1873                if let Some(stake_amount) = stake_amount.get(era, period) {
1874                    if !stake_amount.total().is_zero() {
1875                        dapp_stakes.push((dapp_id, stake_amount.total()));
1876                    }
1877                }
1878            }
1879
1880            // 2.
1881            // Sort by amount staked, in reverse - top dApp will end in the first place, 0th index.
1882            dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1));
1883
1884            let tier_config = TierConfig::<T>::get();
1885
1886            // In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold,
1887            // one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists.
1888            //
1889            // In the current implementation, the dApp with the lower dApp Id has the advantage.
1890            // There is no guarantee this will persist in the future, so it's best for dApps to do their
1891            // best to avoid getting themselves into such situations.
1892
1893            // 3. Calculate rewards.
1894            let tier_rewards = tier_config
1895                .reward_portion
1896                .iter()
1897                .zip(tier_config.slots_per_tier.iter())
1898                .map(|(percent, slots)| {
1899                    if slots.is_zero() {
1900                        Zero::zero()
1901                    } else {
1902                        *percent * dapp_reward_pool / <u16 as Into<Balance>>::into(*slots)
1903                    }
1904                })
1905                .collect::<Vec<_>>();
1906
1907            // 4.
1908            // Iterate over configured tier and potential dApps.
1909            // Each dApp will be assigned to the best possible tier if it satisfies the required condition,
1910            // and tier capacity hasn't been filled yet.
1911            let mut dapp_tiers = BTreeMap::new();
1912            let mut tier_slots = BTreeMap::new();
1913
1914            let mut upper_bound = Balance::zero();
1915            let mut rank_rewards = Vec::new();
1916
1917            for (tier_id, (tier_capacity, lower_bound)) in tier_config
1918                .slots_per_tier
1919                .iter()
1920                .zip(tier_config.tier_thresholds.iter())
1921                .enumerate()
1922            {
1923                // Iterate over dApps until one of two conditions has been met:
1924                // 1. Tier has no more capacity
1925                // 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
1926                for (dapp_id, staked_amount) in dapp_stakes
1927                    .iter()
1928                    .skip(dapp_tiers.len())
1929                    .take_while(|(_, amount)| amount.ge(lower_bound))
1930                    .take(*tier_capacity as usize)
1931                {
1932                    let rank = if T::RankingEnabled::get() {
1933                        RankedTier::find_rank(*lower_bound, upper_bound, *staked_amount)
1934                    } else {
1935                        0
1936                    };
1937                    tier_slots.insert(*dapp_id, RankedTier::new_saturated(tier_id as u8, rank));
1938                }
1939
1940                // sum of all ranks for this tier
1941                let ranks_sum = tier_slots
1942                    .iter()
1943                    .fold(0u32, |accum, (_, x)| accum.saturating_add(x.rank().into()));
1944
1945                let reward_per_rank = if ranks_sum.is_zero() {
1946                    Balance::zero()
1947                } else {
1948                    // calculate reward per rank
1949                    let tier_reward = tier_rewards.get(tier_id).copied().unwrap_or_default();
1950                    let empty_slots = tier_capacity.saturating_sub(tier_slots.len() as u16);
1951                    let remaining_reward = tier_reward.saturating_mul(empty_slots.into());
1952                    // make sure required reward doesn't exceed remaining reward
1953                    let reward_per_rank = tier_reward.saturating_div(RankedTier::MAX_RANK.into());
1954                    let expected_reward_for_ranks =
1955                        reward_per_rank.saturating_mul(ranks_sum.into());
1956                    let reward_for_ranks = expected_reward_for_ranks.min(remaining_reward);
1957                    // re-calculate reward per rank based on available reward
1958                    reward_for_ranks.saturating_div(ranks_sum.into())
1959                };
1960
1961                rank_rewards.push(reward_per_rank);
1962                dapp_tiers.append(&mut tier_slots);
1963                upper_bound = *lower_bound; // current threshold becomes upper bound for next tier
1964            }
1965
1966            // 5.
1967            // Prepare and return tier & rewards info.
1968            // In case rewards creation fails, we just write the default value. This should never happen though.
1969            (
1970                DAppTierRewards::<T::MaxNumberOfContracts, T::NumberOfTiers>::new(
1971                    dapp_tiers,
1972                    tier_rewards,
1973                    period,
1974                    rank_rewards,
1975                )
1976                .unwrap_or_default(),
1977                counter,
1978            )
1979        }
1980
1981        /// Used to handle era & period transitions.
1982        pub(crate) fn era_and_period_handler(
1983            now: BlockNumber,
1984            tier_assignment: TierAssignment,
1985        ) -> Weight {
1986            let mut protocol_state = ActiveProtocolState::<T>::get();
1987
1988            // `ActiveProtocolState` is whitelisted, so we need to account for its read.
1989            let mut consumed_weight = T::DbWeight::get().reads(1);
1990
1991            // We should not modify pallet storage while in maintenance mode.
1992            // This is a safety measure, since maintenance mode is expected to be
1993            // enabled in case some misbehavior or corrupted storage is detected.
1994            if protocol_state.maintenance {
1995                return consumed_weight;
1996            }
1997
1998            // Inform observers about the upcoming new era, if it's the case.
1999            if protocol_state.next_era_start == now.saturating_add(1) {
2000                consumed_weight
2001                    .saturating_accrue(Self::notify_block_before_new_era(&protocol_state));
2002            }
2003
2004            // Nothing to do if it's not new era
2005            if !protocol_state.is_new_era(now) {
2006                return consumed_weight;
2007            }
2008
2009            // At this point it's clear that an era change will happen
2010            let mut era_info = CurrentEraInfo::<T>::get();
2011
2012            let current_era = protocol_state.era;
2013            let next_era = current_era.saturating_add(1);
2014            let (maybe_period_event, era_reward) = match protocol_state.subperiod() {
2015                // Voting subperiod only lasts for one 'prolonged' era
2016                Subperiod::Voting => {
2017                    // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod.
2018                    let era_reward = EraReward {
2019                        staker_reward_pool: Balance::zero(),
2020                        staked: era_info.total_staked_amount(),
2021                        dapp_reward_pool: Balance::zero(),
2022                    };
2023
2024                    let next_subperiod_start_era = next_era
2025                        .saturating_add(T::CycleConfiguration::eras_per_build_and_earn_subperiod());
2026                    let build_and_earn_start_block =
2027                        now.saturating_add(T::CycleConfiguration::blocks_per_era());
2028                    protocol_state.advance_to_next_subperiod(
2029                        next_subperiod_start_era,
2030                        build_and_earn_start_block,
2031                    );
2032
2033                    era_info.migrate_to_next_era(Some(protocol_state.subperiod()));
2034
2035                    consumed_weight
2036                        .saturating_accrue(T::WeightInfo::on_initialize_voting_to_build_and_earn());
2037
2038                    (
2039                        Some(Event::<T>::NewSubperiod {
2040                            subperiod: protocol_state.subperiod(),
2041                            number: protocol_state.period_number(),
2042                        }),
2043                        era_reward,
2044                    )
2045                }
2046                Subperiod::BuildAndEarn => {
2047                    let staked = era_info.total_staked_amount();
2048                    let (staker_reward_pool, dapp_reward_pool) =
2049                        T::StakingRewardHandler::staker_and_dapp_reward_pools(staked);
2050                    let era_reward = EraReward {
2051                        staker_reward_pool,
2052                        staked,
2053                        dapp_reward_pool,
2054                    };
2055
2056                    // Distribute dapps into tiers, write it into storage
2057                    //
2058                    // To help with benchmarking, it's possible to omit real tier calculation using the `Dummy` approach.
2059                    // This must never be used in production code, obviously.
2060                    let (dapp_tier_rewards, counter) = match tier_assignment {
2061                        TierAssignment::Real => Self::get_dapp_tier_assignment_and_rewards(
2062                            current_era,
2063                            protocol_state.period_number(),
2064                            dapp_reward_pool,
2065                        ),
2066                        #[cfg(feature = "runtime-benchmarks")]
2067                        TierAssignment::Dummy => (DAppTierRewardsFor::<T>::default(), 0),
2068                    };
2069                    DAppTiers::<T>::insert(&current_era, dapp_tier_rewards);
2070
2071                    consumed_weight
2072                        .saturating_accrue(T::WeightInfo::dapp_tier_assignment(counter.into()));
2073
2074                    // Switch to `Voting` period if conditions are met.
2075                    if protocol_state.period_info.is_next_period(next_era) {
2076                        // Store info about period end
2077                        let bonus_reward_pool = T::StakingRewardHandler::bonus_reward_pool();
2078                        PeriodEnd::<T>::insert(
2079                            &protocol_state.period_number(),
2080                            PeriodEndInfo {
2081                                bonus_reward_pool,
2082                                total_vp_stake: era_info.staked_amount(Subperiod::Voting),
2083                                final_era: current_era,
2084                            },
2085                        );
2086
2087                        // For the sake of consistency we treat the whole `Voting` period as a single era.
2088                        // This means no special handling is required for this period, it only lasts potentially longer than a single standard era.
2089                        let next_subperiod_start_era = next_era.saturating_add(1);
2090                        let voting_period_length = Self::blocks_per_voting_period();
2091                        let next_era_start_block = now.saturating_add(voting_period_length);
2092
2093                        protocol_state.advance_to_next_subperiod(
2094                            next_subperiod_start_era,
2095                            next_era_start_block,
2096                        );
2097
2098                        era_info.migrate_to_next_era(Some(protocol_state.subperiod()));
2099
2100                        // Update historical cleanup marker.
2101                        // Must be called with the new period number.
2102                        Self::update_cleanup_marker(protocol_state.period_number());
2103
2104                        consumed_weight.saturating_accrue(
2105                            T::WeightInfo::on_initialize_build_and_earn_to_voting(),
2106                        );
2107
2108                        (
2109                            Some(Event::<T>::NewSubperiod {
2110                                subperiod: protocol_state.subperiod(),
2111                                number: protocol_state.period_number(),
2112                            }),
2113                            era_reward,
2114                        )
2115                    } else {
2116                        let next_era_start_block =
2117                            now.saturating_add(T::CycleConfiguration::blocks_per_era());
2118                        protocol_state.next_era_start = next_era_start_block;
2119
2120                        era_info.migrate_to_next_era(None);
2121
2122                        consumed_weight.saturating_accrue(
2123                            T::WeightInfo::on_initialize_build_and_earn_to_build_and_earn(),
2124                        );
2125
2126                        (None, era_reward)
2127                    }
2128                }
2129            };
2130
2131            // Update storage items
2132            protocol_state.era = next_era;
2133            ActiveProtocolState::<T>::put(protocol_state);
2134
2135            CurrentEraInfo::<T>::put(era_info);
2136
2137            let era_span_index = Self::era_reward_span_index(current_era);
2138            let mut span = EraRewards::<T>::get(&era_span_index).unwrap_or(EraRewardSpan::new());
2139            if let Err(_) = span.push(current_era, era_reward) {
2140                // This must never happen but we log the error just in case.
2141                log::error!(
2142                    target: LOG_TARGET,
2143                    "Failed to push era {} into the era reward span.",
2144                    current_era
2145                );
2146            }
2147            EraRewards::<T>::insert(&era_span_index, span);
2148
2149            // Re-calculate tier configuration for the upcoming new era
2150            let tier_params = StaticTierParams::<T>::get();
2151            let average_price = T::NativePriceProvider::average_price();
2152            let total_issuance = T::Currency::total_issuance();
2153
2154            let new_tier_config =
2155                TierConfig::<T>::get().calculate_new(&tier_params, average_price, total_issuance);
2156
2157            // Validate new tier configuration
2158            if new_tier_config.is_valid() {
2159                TierConfig::<T>::put(new_tier_config);
2160            } else {
2161                log::warn!(
2162                    target: LOG_TARGET,
2163                    "New tier configuration is invalid for era {}, preserving old one.",
2164                    next_era
2165                );
2166            }
2167
2168            Self::deposit_event(Event::<T>::NewEra { era: next_era });
2169            if let Some(period_event) = maybe_period_event {
2170                Self::deposit_event(period_event);
2171            }
2172
2173            consumed_weight
2174        }
2175
2176        /// Used to notify observers about the upcoming new era in the next block.
2177        fn notify_block_before_new_era(protocol_state: &ProtocolState) -> Weight {
2178            let next_era = protocol_state.era.saturating_add(1);
2179            T::Observers::block_before_new_era(next_era)
2180        }
2181
2182        /// Updates the cleanup marker with the new oldest valid era if possible.
2183        ///
2184        /// It's possible that the call will be a no-op since we haven't advanced enough periods yet.
2185        fn update_cleanup_marker(new_period_number: PeriodNumber) {
2186            // 1. Find out the latest expired period; rewards can no longer be claimed for it or any older period.
2187            let latest_expired_period = match new_period_number
2188                .checked_sub(T::RewardRetentionInPeriods::get().saturating_add(1))
2189            {
2190                Some(period) if !period.is_zero() => period,
2191                // Haven't advanced enough periods to have any expired entries.
2192                _ => return,
2193            };
2194
2195            // 2. Find the oldest valid era for which rewards can still be claimed.
2196            //    Technically, this will be `Voting` subperiod era but it doesn't matter.
2197            //
2198            //    Also, remove the expired `PeriodEnd` entry since it's no longer needed.
2199            let oldest_valid_era = match PeriodEnd::<T>::take(latest_expired_period) {
2200                Some(period_end_info) => period_end_info.final_era.saturating_add(1),
2201                None => {
2202                    // Should never happen but nothing we can do if it does.
2203                    log::error!(
2204                        target: LOG_TARGET,
2205                        "No `PeriodEnd` entry for the expired period: {}",
2206                        latest_expired_period
2207                    );
2208                    return;
2209                }
2210            };
2211
2212            // 3. Update the cleanup marker with the new oldest valid era.
2213            HistoryCleanupMarker::<T>::mutate(|marker| {
2214                marker.oldest_valid_era = oldest_valid_era;
2215            });
2216        }
2217
2218        /// Attempt to cleanup some expired entries, if enough remaining weight & applicable entries exist.
2219        ///
2220        /// Returns consumed weight.
2221        fn expired_entry_cleanup(remaining_weight: &Weight) -> Weight {
2222            // Need to be able to process one full pass
2223            if remaining_weight.any_lt(T::WeightInfo::on_idle_cleanup()) {
2224                return Weight::zero();
2225            }
2226
2227            // Get the cleanup marker and ensure we have pending cleanups.
2228            let mut cleanup_marker = HistoryCleanupMarker::<T>::get();
2229            if !cleanup_marker.has_pending_cleanups() {
2230                return T::DbWeight::get().reads(1);
2231            }
2232
2233            // 1. Attempt to cleanup one expired `EraRewards` entry.
2234            if cleanup_marker.era_reward_index < cleanup_marker.oldest_valid_era {
2235                if let Some(era_reward) = EraRewards::<T>::get(cleanup_marker.era_reward_index) {
2236                    // If oldest valid era comes AFTER this span, it's safe to delete it.
2237                    if era_reward.last_era() < cleanup_marker.oldest_valid_era {
2238                        EraRewards::<T>::remove(cleanup_marker.era_reward_index);
2239                        cleanup_marker
2240                            .era_reward_index
2241                            .saturating_accrue(T::EraRewardSpanLength::get());
2242                    }
2243                } else {
2244                    // Can happen if the entry is part of history before dApp staking v3
2245                    log::warn!(
2246                        target: LOG_TARGET,
2247                        "Era rewards span for era {} is missing, but cleanup marker is set.",
2248                        cleanup_marker.era_reward_index
2249                    );
2250                    cleanup_marker
2251                        .era_reward_index
2252                        .saturating_accrue(T::EraRewardSpanLength::get());
2253                }
2254            }
2255
2256            // 2. Attempt to cleanup one expired `DAppTiers` entry.
2257            if cleanup_marker.dapp_tiers_index < cleanup_marker.oldest_valid_era {
2258                DAppTiers::<T>::remove(cleanup_marker.dapp_tiers_index);
2259                cleanup_marker.dapp_tiers_index.saturating_inc();
2260            }
2261
2262            // Store the updated cleanup marker
2263            HistoryCleanupMarker::<T>::put(cleanup_marker);
2264
2265            // We could try & cleanup more entries, but since it's not a critical operation and can happen whenever,
2266            // we opt for the simpler solution where only 1 entry per block is cleaned up.
2267            // It can be changed though.
2268
2269            // It could end up being less than this weight, but this won't occur often enough to be important.
2270            T::WeightInfo::on_idle_cleanup()
2271        }
2272
2273        /// Internal function that executes the `claim_unlocked` logic for the specified account.
2274        fn internal_claim_unlocked(account: T::AccountId) -> DispatchResultWithPostInfo {
2275            let mut ledger = Ledger::<T>::get(&account);
2276
2277            let current_block = frame_system::Pallet::<T>::block_number();
2278            let amount = ledger.claim_unlocked(current_block.saturated_into());
2279            ensure!(amount > Zero::zero(), Error::<T>::NoUnlockedChunksToClaim);
2280
2281            // In case it's full unlock, account is exiting dApp staking, ensure all storage is cleaned up.
2282            let removed_entries = if ledger.is_empty() {
2283                let _ = StakerInfo::<T>::clear_prefix(&account, ledger.contract_stake_count, None);
2284                ledger.contract_stake_count
2285            } else {
2286                0
2287            };
2288
2289            Self::update_ledger(&account, ledger)?;
2290            CurrentEraInfo::<T>::mutate(|era_info| {
2291                era_info.unlocking_removed(amount);
2292            });
2293
2294            Self::deposit_event(Event::<T>::ClaimedUnlocked { account, amount });
2295
2296            Ok(Some(T::WeightInfo::claim_unlocked(removed_entries)).into())
2297        }
2298
2299        /// Internal function that executes the `claim_staker_rewards_` logic for the specified account.
2300        fn internal_claim_staker_rewards_for(account: T::AccountId) -> DispatchResultWithPostInfo {
2301            let mut ledger = Ledger::<T>::get(&account);
2302            let staked_period = ledger
2303                .staked_period()
2304                .ok_or(Error::<T>::NoClaimableRewards)?;
2305
2306            // Check if the rewards have expired
2307            let protocol_state = ActiveProtocolState::<T>::get();
2308            ensure!(
2309                staked_period >= Self::oldest_claimable_period(protocol_state.period_number()),
2310                Error::<T>::RewardExpired
2311            );
2312
2313            // Calculate the reward claim span
2314            let earliest_staked_era = ledger
2315                .earliest_staked_era()
2316                .ok_or(Error::<T>::InternalClaimStakerError)?;
2317            let era_rewards =
2318                EraRewards::<T>::get(Self::era_reward_span_index(earliest_staked_era))
2319                    .ok_or(Error::<T>::NoClaimableRewards)?;
2320
2321            // The last era for which we can theoretically claim rewards.
2322            // And indicator if we know the period's ending era.
2323            let (last_period_era, period_end) = if staked_period == protocol_state.period_number() {
2324                (protocol_state.era.saturating_sub(1), None)
2325            } else {
2326                PeriodEnd::<T>::get(&staked_period)
2327                    .map(|info| (info.final_era, Some(info.final_era)))
2328                    .ok_or(Error::<T>::InternalClaimStakerError)?
2329            };
2330
2331            // The last era for which we can claim rewards for this account.
2332            let last_claim_era = era_rewards.last_era().min(last_period_era);
2333
2334            // Get chunks for reward claiming
2335            let rewards_iter =
2336                ledger
2337                    .claim_up_to_era(last_claim_era, period_end)
2338                    .map_err(|err| match err {
2339                        AccountLedgerError::NothingToClaim => Error::<T>::NoClaimableRewards,
2340                        _ => Error::<T>::InternalClaimStakerError,
2341                    })?;
2342
2343            // Calculate rewards
2344            let mut rewards: Vec<_> = Vec::new();
2345            let mut reward_sum = Balance::zero();
2346            for (era, amount) in rewards_iter {
2347                let era_reward = era_rewards
2348                    .get(era)
2349                    .ok_or(Error::<T>::InternalClaimStakerError)?;
2350
2351                // Optimization, and zero-division protection
2352                if amount.is_zero() || era_reward.staked.is_zero() {
2353                    continue;
2354                }
2355                let staker_reward = Perbill::from_rational(amount, era_reward.staked)
2356                    * era_reward.staker_reward_pool;
2357
2358                rewards.push((era, staker_reward));
2359                reward_sum.saturating_accrue(staker_reward);
2360            }
2361            let rewards_len: u32 = rewards.len().unique_saturated_into();
2362
2363            T::StakingRewardHandler::payout_reward(&account, reward_sum)
2364                .map_err(|_| Error::<T>::RewardPayoutFailed)?;
2365
2366            Self::update_ledger(&account, ledger)?;
2367
2368            rewards.into_iter().for_each(|(era, reward)| {
2369                Self::deposit_event(Event::<T>::Reward {
2370                    account: account.clone(),
2371                    era,
2372                    amount: reward,
2373                });
2374            });
2375
2376            Ok(Some(if period_end.is_some() {
2377                T::WeightInfo::claim_staker_rewards_past_period(rewards_len)
2378            } else {
2379                T::WeightInfo::claim_staker_rewards_ongoing_period(rewards_len)
2380            })
2381            .into())
2382        }
2383
2384        /// Internal function that executes the `claim_bonus_reward` logic for the specified account & smart contract.
2385        fn internal_claim_bonus_reward_for(
2386            account: T::AccountId,
2387            smart_contract: T::SmartContract,
2388        ) -> DispatchResult {
2389            let staker_info = StakerInfo::<T>::get(&account, &smart_contract)
2390                .ok_or(Error::<T>::NoClaimableRewards)?;
2391            let protocol_state = ActiveProtocolState::<T>::get();
2392
2393            // Ensure:
2394            // 1. Period for which rewards are being claimed has ended.
2395            // 2. Account has maintained an eligible bonus status.
2396            // 3. Rewards haven't expired.
2397            let staked_period = staker_info.period_number();
2398            ensure!(
2399                staked_period < protocol_state.period_number(),
2400                Error::<T>::NoClaimableRewards
2401            );
2402            ensure!(
2403                staker_info.is_bonus_eligible(),
2404                Error::<T>::NotEligibleForBonusReward
2405            );
2406            ensure!(
2407                staker_info.period_number()
2408                    >= Self::oldest_claimable_period(protocol_state.period_number()),
2409                Error::<T>::RewardExpired
2410            );
2411
2412            let period_end_info =
2413                PeriodEnd::<T>::get(&staked_period).ok_or(Error::<T>::InternalClaimBonusError)?;
2414            // Defensive check - we should never get this far in function if no voting period stake exists.
2415            ensure!(
2416                !period_end_info.total_vp_stake.is_zero(),
2417                Error::<T>::InternalClaimBonusError
2418            );
2419
2420            let eligible_amount = staker_info.staked_amount(Subperiod::Voting);
2421            let bonus_reward =
2422                Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake)
2423                    * period_end_info.bonus_reward_pool;
2424
2425            T::StakingRewardHandler::payout_reward(&account, bonus_reward)
2426                .map_err(|_| Error::<T>::RewardPayoutFailed)?;
2427
2428            // Cleanup entry since the reward has been claimed
2429            StakerInfo::<T>::remove(&account, &smart_contract);
2430            Ledger::<T>::mutate(&account, |ledger| {
2431                ledger.contract_stake_count.saturating_dec();
2432            });
2433
2434            Self::deposit_event(Event::<T>::BonusReward {
2435                account: account.clone(),
2436                smart_contract,
2437                period: staked_period,
2438                amount: bonus_reward,
2439            });
2440
2441            Ok(())
2442        }
2443
2444        /// Internal function to transition the dApp staking protocol maintenance mode.
2445        /// Ensure this method is **not exposed publicly** and is only used for legitimate maintenance mode transitions invoked by privileged or trusted logic,
2446        /// such as `T::ManagerOrigin` or a safe-mode enter/exit notification.
2447        fn set_maintenance_mode(enabled: bool) {
2448            ActiveProtocolState::<T>::mutate(|state| state.maintenance = enabled);
2449            Self::deposit_event(Event::<T>::MaintenanceMode { enabled });
2450        }
2451
2452        /// Ensure the correctness of the state of this pallet.
2453        #[cfg(any(feature = "try-runtime", test))]
2454        pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
2455            Self::try_state_protocol()?;
2456            Self::try_state_next_dapp_id()?;
2457            Self::try_state_integrated_dapps()?;
2458            Self::try_state_tiers()?;
2459            Self::try_state_ledger()?;
2460            Self::try_state_contract_stake()?;
2461            Self::try_state_era_rewards()?;
2462            Self::try_state_era_info()?;
2463
2464            Ok(())
2465        }
2466
2467        /// ### Invariants of active protocol storage items
2468        ///
2469        /// 1. [`PeriodInfo`] number in [`ActiveProtocolState`] must always be greater than the number of elements in [`PeriodEnd`].
2470        /// 2. Ensures the `era` number and `next_era_start` block number are valid.
2471        #[cfg(any(feature = "try-runtime", test))]
2472        pub fn try_state_protocol() -> Result<(), sp_runtime::TryRuntimeError> {
2473            let protocol_state = ActiveProtocolState::<T>::get();
2474
2475            // Invariant 1
2476            if PeriodEnd::<T>::iter().count() >= protocol_state.period_info.number as usize {
2477                return Err("Number of periods in `PeriodEnd` exceeds or is equal to actual `PeriodInfo` number.".into());
2478            }
2479
2480            // Invariant 2
2481            if protocol_state.era == 0 {
2482                return Err("Invalid era number in ActiveProtocolState.".into());
2483            }
2484
2485            let current_block: BlockNumber =
2486                frame_system::Pallet::<T>::block_number().saturated_into();
2487            if current_block > protocol_state.next_era_start {
2488                return Err(
2489                    "Next era start block number is in the past in ActiveProtocolState.".into(),
2490                );
2491            }
2492
2493            Ok(())
2494        }
2495
2496        /// ### Invariants of NextDAppId
2497        ///
2498        /// 1. [`NextDAppId`] must always be greater than or equal to the number of dapps in [`IntegratedDApps`].
2499        /// 2. [`NextDAppId`] must always be greater than or equal to the number of contracts in [`ContractStake`].
2500        #[cfg(any(feature = "try-runtime", test))]
2501        pub fn try_state_next_dapp_id() -> Result<(), sp_runtime::TryRuntimeError> {
2502            let next_dapp_id = NextDAppId::<T>::get();
2503
2504            // Invariant 1
2505            if next_dapp_id < IntegratedDApps::<T>::count() as u16 {
2506                return Err("Number of integrated dapps is greater than NextDAppId.".into());
2507            }
2508
2509            // Invariant 2
2510            if next_dapp_id < ContractStake::<T>::iter().count() as u16 {
2511                return Err("Number of contract stake infos is greater than NextDAppId.".into());
2512            }
2513
2514            Ok(())
2515        }
2516
2517        /// ### Invariants of IntegratedDApps
2518        ///
2519        /// 1. The number of entries in [`IntegratedDApps`] should not exceed the [`T::MaxNumberOfContracts`] constant.
2520        #[cfg(any(feature = "try-runtime", test))]
2521        pub fn try_state_integrated_dapps() -> Result<(), sp_runtime::TryRuntimeError> {
2522            let integrated_dapps_count = IntegratedDApps::<T>::count();
2523            let max_number_of_contracts = T::MaxNumberOfContracts::get();
2524
2525            if integrated_dapps_count > max_number_of_contracts {
2526                return Err("Number of integrated dapps exceeds the maximum allowed.".into());
2527            }
2528
2529            Ok(())
2530        }
2531
2532        /// ### Invariants of StaticTierParams and TierConfig
2533        ///
2534        /// 1. The [`T::NumberOfTiers`] constant must always be equal to the number of `slot_distribution`, `reward_portion`, `tier_thresholds` in [`StaticTierParams`].
2535        /// 2. The [`T::NumberOfTiers`] constant must always be equal to the number of `slots_per_tier`, `reward_portion`, `tier_thresholds` in [`TierConfig`].
2536        #[cfg(any(feature = "try-runtime", test))]
2537        pub fn try_state_tiers() -> Result<(), sp_runtime::TryRuntimeError> {
2538            let nb_tiers = T::NumberOfTiers::get();
2539            let tier_params = StaticTierParams::<T>::get();
2540            let tier_config = TierConfig::<T>::get();
2541
2542            // Invariant 1
2543            if nb_tiers != tier_params.slot_distribution.len() as u32 {
2544                return Err(
2545                    "Number of tiers is incorrect in slot_distribution in StaticTierParams.".into(),
2546                );
2547            }
2548            if nb_tiers != tier_params.reward_portion.len() as u32 {
2549                return Err(
2550                    "Number of tiers is incorrect in reward_portion in StaticTierParams.".into(),
2551                );
2552            }
2553            if nb_tiers != tier_params.tier_thresholds.len() as u32 {
2554                return Err(
2555                    "Number of tiers is incorrect in tier_thresholds in StaticTierParams.".into(),
2556                );
2557            }
2558
2559            // Invariant 2
2560            if nb_tiers != tier_config.slots_per_tier.len() as u32 {
2561                return Err(
2562                    "Number of tiers is incorrect in slots_per_tier in StaticTierParams.".into(),
2563                );
2564            }
2565            if nb_tiers != tier_config.reward_portion.len() as u32 {
2566                return Err(
2567                    "Number of tiers is incorrect in reward_portion in StaticTierParams.".into(),
2568                );
2569            }
2570            if nb_tiers != tier_config.tier_thresholds.len() as u32 {
2571                return Err(
2572                    "Number of tiers is incorrect in tier_thresholds in StaticTierParams.".into(),
2573                );
2574            }
2575
2576            Ok(())
2577        }
2578
2579        /// ### Invariants of Ledger
2580        ///
2581        /// 1. Iterating over all [`Ledger`] accounts should yield the correct locked and stakes amounts compared to current era in [`CurrentEraInfo`].
2582        /// 2. The number of unlocking chunks in [`Ledger`] for any account should not exceed the [`T::MaxUnlockingChunks`] constant.
2583        /// 3. Each staking entry in [`Ledger`] should be greater than or equal to the [`T::MinimumStakeAmount`] constant.
2584        /// 4. Each locking entry in [`Ledger`] should be greater than or equal to the [`T::MinimumLockedAmount`] constant.
2585        /// 5. The number of staking entries per account in [`Ledger`] should not exceed the [`T::MaxNumberOfStakedContracts`] constant.
2586        #[cfg(any(feature = "try-runtime", test))]
2587        pub fn try_state_ledger() -> Result<(), sp_runtime::TryRuntimeError> {
2588            let current_period_number = ActiveProtocolState::<T>::get().period_number();
2589            let current_era_info = CurrentEraInfo::<T>::get();
2590            let next_era_total_stake = current_era_info.total_staked_amount_next_era();
2591
2592            // Yield amounts in [`Ledger`]
2593            let mut ledger_total_stake = Balance::zero();
2594            let mut ledger_total_locked = Balance::zero();
2595            let mut ledger_total_unlocking = Balance::zero();
2596
2597            for (_, ledger) in Ledger::<T>::iter() {
2598                let account_stake = ledger.staked_amount(current_period_number);
2599
2600                ledger_total_stake += account_stake;
2601                ledger_total_locked += ledger.active_locked_amount();
2602                ledger_total_unlocking += ledger.unlocking_amount();
2603
2604                // Invariant 2
2605                if ledger.unlocking.len() > T::MaxUnlockingChunks::get() as usize {
2606                    return Err("An account exceeds the maximum unlocking chunks.".into());
2607                }
2608
2609                // Invariant 3
2610                if account_stake > Balance::zero() && account_stake < T::MinimumStakeAmount::get() {
2611                    return Err(
2612                        "An account has a stake amount lower than the minimum allowed.".into(),
2613                    );
2614                }
2615
2616                // Invariant 4
2617                if ledger.active_locked_amount() > Balance::zero()
2618                    && ledger.active_locked_amount() < T::MinimumLockedAmount::get()
2619                {
2620                    return Err(
2621                        "An account has a locked amount lower than the minimum allowed.".into(),
2622                    );
2623                }
2624
2625                // Invariant 5
2626                if ledger.contract_stake_count > T::MaxNumberOfStakedContracts::get() {
2627                    return Err("An account exceeds the maximum number of staked contracts.".into());
2628                }
2629            }
2630
2631            // Invariant 1
2632            if ledger_total_stake != next_era_total_stake {
2633                return Err(
2634                    "Mismatch between Ledger total staked amounts and CurrentEraInfo total.".into(),
2635                );
2636            }
2637
2638            if ledger_total_locked != current_era_info.total_locked {
2639                return Err(
2640                    "Mismatch between Ledger total locked amounts and CurrentEraInfo total.".into(),
2641                );
2642            }
2643
2644            if ledger_total_unlocking != current_era_info.unlocking {
2645                return Err(
2646                    "Mismatch between Ledger total unlocked amounts and CurrentEraInfo total."
2647                        .into(),
2648                );
2649            }
2650
2651            Ok(())
2652        }
2653
2654        /// ### Invariants of ContractStake
2655        ///
2656        /// 1. Each staking entry in [`ContractStake`] should be greater than or equal to the [`T::MinimumStakeAmount`] constant.
2657        #[cfg(any(feature = "try-runtime", test))]
2658        pub fn try_state_contract_stake() -> Result<(), sp_runtime::TryRuntimeError> {
2659            let current_period_number = ActiveProtocolState::<T>::get().period_number();
2660
2661            for (_, contract) in ContractStake::<T>::iter() {
2662                let contract_stake = contract.total_staked_amount(current_period_number);
2663
2664                // Invariant 1
2665                if contract_stake > Balance::zero() && contract_stake < T::MinimumStakeAmount::get()
2666                {
2667                    return Err(
2668                        "A contract has a staked amount lower than the minimum allowed.".into(),
2669                    );
2670                }
2671            }
2672
2673            Ok(())
2674        }
2675
2676        /// ### Invariants of EraRewards
2677        ///
2678        /// 1. Era number in [`DAppTiers`] must also be stored in one of the span of [`EraRewards`].
2679        /// 2. Each span length entry in [`EraRewards`] should be lower than or equal to the [`T::EraRewardSpanLength`] constant.
2680        #[cfg(any(feature = "try-runtime", test))]
2681        pub fn try_state_era_rewards() -> Result<(), sp_runtime::TryRuntimeError> {
2682            let era_rewards = EraRewards::<T>::iter().collect::<Vec<_>>();
2683            let dapp_tiers = DAppTiers::<T>::iter().collect::<Vec<_>>();
2684
2685            // Invariant 1
2686            for (era, _) in &dapp_tiers {
2687                let mut found = false;
2688                for (_, span) in &era_rewards {
2689                    if *era >= span.first_era() && *era <= span.last_era() {
2690                        found = true;
2691                        break;
2692                    }
2693                }
2694
2695                // Invariant 1
2696                if !found {
2697                    return Err("Era in DAppTiers is not found in any span in EraRewards.".into());
2698                }
2699            }
2700
2701            for (_, span) in &era_rewards {
2702                // Invariant 3
2703                if span.len() > T::EraRewardSpanLength::get() as usize {
2704                    return Err(
2705                        "Span length for a era exceeds the maximum allowed span length.".into(),
2706                    );
2707                }
2708            }
2709
2710            Ok(())
2711        }
2712
2713        /// ### Invariants of `EraInfo`
2714        ///
2715        /// 1. StakerInfo total voting stake == CurrentEraInfo.next_stake_amount
2716        /// 2. Current voting stake ≤ Next voting stake (not equal due to possible moves)
2717        #[cfg(any(feature = "try-runtime", test))]
2718        pub fn try_state_era_info() -> Result<(), sp_runtime::TryRuntimeError> {
2719            let protocol_state = ActiveProtocolState::<T>::get();
2720            let current_period = protocol_state.period_number();
2721            let era_info = CurrentEraInfo::<T>::get();
2722
2723            let current_voting = era_info.staked_amount(Subperiod::Voting);
2724            let next_voting = era_info.staked_amount_next_era(Subperiod::Voting);
2725
2726            // Yield voting stake amounts in [`StakerInfo`] for the current period
2727            let voting_total_staked: Balance = StakerInfo::<T>::iter()
2728                .filter(|(_, _, info)| info.period_number() == current_period)
2729                .map(|(_, _, info)| info.staked_amount(Subperiod::Voting))
2730                .sum();
2731
2732            // Invariant 1
2733            ensure!(
2734                voting_total_staked == next_voting,
2735                "StakerInfo voting total != CurrentEraInfo.next voting stake"
2736            );
2737
2738            // Invariant 2
2739            ensure!(
2740                current_voting <= next_voting,
2741                "Current voting stake > Next voting stake for same period"
2742            );
2743
2744            Ok(())
2745        }
2746    }
2747
2748    /// Implementation of the `SafeModeNotify` trait for the `DappStaking` pallet.
2749    /// This integration ensures that the dApp staking protocol transitions to and from
2750    /// maintenance mode when the runtime enters or exits safe mode.
2751    impl<T: Config> SafeModeNotify for Pallet<T> {
2752        fn entered() {
2753            Self::set_maintenance_mode(true);
2754        }
2755
2756        fn exited() {
2757            Self::set_maintenance_mode(false);
2758        }
2759    }
2760}