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