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