pallet_inflation/
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//! # Inflation Handler Pallet
20//!
21//! ## Overview
22//!
23//! This pallet's main responsibility is handling inflation calculation & distribution.
24//!
25//! Inflation configuration is calculated periodically, according to the inflation parameters.
26//! Based on this configuration, rewards are paid out - either per block or on demand.
27//!
28//! ## Cycles, Periods, Eras
29//!
30//! At the start of each cycle, the inflation configuration is recalculated.
31//!
32//! Cycle can be considered as a 'year' in the Astar network.
33//! When cycle starts, inflation is calculated according to the total issuance at that point in time.
34//! E.g. if 'yearly' inflation is set to be 7%, and total issuance is 200 ASTR, then the max inflation for that cycle will be 14 ASTR.
35//!
36//! Each cycle consists of one or more `periods`.
37//! Periods are integral part of dApp staking protocol, allowing dApps to promote themselves, attract stakers and earn rewards.
38//! At the end of each period, all stakes are reset, and dApps need to repeat the process.
39//!
40//! Each period consists of two subperiods: `Voting` and `Build&Earn`.
41//! Length of these subperiods is expressed in eras. An `era` is the core _time unit_ in dApp staking protocol.
42//! When an era ends, in `Build&Earn` subperiod, rewards for dApps are calculated & assigned.
43//!
44//! Era's length is expressed in blocks. E.g. an era can last for 7200 blocks, which is approximately 1 day for 12 second block time.
45//!
46//! `Build&Earn` subperiod length is expressed in eras. E.g. if `Build&Earn` subperiod lasts for 5 eras, it means that during that subperiod,
47//! dApp rewards will be calculated & assigned 5 times in total. Also, 5 distinct eras will change during that subperiod. If e.g. `Build&Earn` started at era 100,
48//! with 5 eras per `Build&Earn` subperiod, then the subperiod will end at era 105.
49//!
50//! `Voting` subperiod always comes before `Build&Earn` subperiod. Its length is also expressed in eras, although it has to be interpreted a bit differently.
51//! Even though `Voting` can last for more than 1 era in respect of length, it always takes exactly 1 era.
52//! What this means is that if `Voting` lasts for 3 eras, and each era lasts 7200 blocks, then `Voting` will last for 21600 blocks.
53//! But unlike `Build&Earn` subperiod, `Voting` will only take up one 'numerical' era. So if `Voting` starts at era 110, it will end at era 11.
54//!
55//! #### Example
56//! * Cycle length: 4 periods
57//! * `Voting` length: 10 eras
58//! * `Build&Earn` length: 81 eras
59//! * Era length: 7200 blocks
60//!
61//! This would mean that cycle lasts for roughly 364 days (4 * (10 + 81)).
62//!
63//! ## Recalculation
64//!
65//! When new cycle begins, inflation configuration is recalculated according to the inflation parameters & total issuance at that point in time.
66//! Based on the max inflation rate, rewards for different network actors are calculated.
67//!
68//! Some rewards are calculated to be paid out per block, while some are per era or per period.
69//!
70//! ## Rewards
71//!
72//! ### Collator & Treasury Rewards
73//!
74//! These are paid out at the beginning of each block & are fixed amounts.
75//!
76//! ### Staker Rewards
77//!
78//! Staker rewards are paid out per staker, _on-demand_.
79//! However, reward pool for an era is calculated at the end of each era.
80//!
81//! `era_reward_pool = base_staker_reward_pool_per_era + adjustable_staker_reward_pool_per_era`
82//!
83//! While the base staker reward pool is fixed, the adjustable part is calculated according to the total value staked & the ideal staking rate.
84//!
85//! ### dApp Rewards
86//!
87//! dApp rewards are paid out per dApp, _on-demand_. The reward is decided by the dApp staking protocol, or the tier system to be more precise.
88//! This pallet only provides the total reward pool for all dApps per era.
89//!
90//! # Interface
91//!
92//! ## StakingRewardHandler
93//!
94//! This pallet implements `StakingRewardHandler` trait, which is used by the dApp staking protocol to get reward pools & distribute rewards.
95//!
96
97#![cfg_attr(not(feature = "std"), no_std)]
98
99pub use pallet::*;
100
101use astar_primitives::{
102    dapp_staking::{
103        CycleConfiguration, EraNumber, Observer as DappStakingObserver, StakingRewardHandler,
104    },
105    Balance,
106};
107use frame_support::{
108    pallet_prelude::*,
109    traits::{
110        fungible::{Balanced, Credit, Inspect},
111        tokens::Precision,
112    },
113    DefaultNoBound,
114};
115use frame_system::{ensure_root, pallet_prelude::*};
116use serde::{Deserialize, Serialize};
117use sp_runtime::{
118    traits::{CheckedAdd, Zero},
119    Perquintill,
120};
121use sp_std::marker::PhantomData;
122
123pub mod weights;
124pub use weights::WeightInfo;
125
126#[cfg(any(feature = "runtime-benchmarks"))]
127pub mod benchmarking;
128
129pub mod migration;
130#[cfg(test)]
131mod mock;
132#[cfg(test)]
133mod tests;
134
135#[frame_support::pallet]
136pub mod pallet {
137    use super::*;
138
139    /// The current storage version.
140    pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
141
142    #[pallet::pallet]
143    #[pallet::storage_version(STORAGE_VERSION)]
144    pub struct Pallet<T>(PhantomData<T>);
145
146    // Negative imbalance type of this pallet.
147    pub(crate) type CreditOf<T> =
148        Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
149
150    #[pallet::config]
151    pub trait Config: frame_system::Config {
152        type Currency: Balanced<Self::AccountId, Balance = Balance>;
153
154        /// Handler for 'per-block' payouts.
155        type PayoutPerBlock: PayoutPerBlock<CreditOf<Self>>;
156
157        /// Cycle ('year') configuration - covers periods, subperiods, eras & blocks.
158        type CycleConfiguration: CycleConfiguration;
159
160        /// Weight information for extrinsics in this pallet.
161        type WeightInfo: WeightInfo;
162    }
163
164    #[pallet::event]
165    #[pallet::generate_deposit(pub(crate) fn deposit_event)]
166    pub enum Event<T: Config> {
167        /// Inflation parameters have been force changed. This will have effect on the next inflation recalculation.
168        InflationParametersForceChanged,
169        /// Inflation recalculation has been forced.
170        ForcedInflationRecalculation { config: InflationConfiguration },
171        /// New inflation configuration has been set.
172        NewInflationConfiguration { config: InflationConfiguration },
173    }
174
175    #[pallet::error]
176    pub enum Error<T> {
177        /// Sum of all parts must be one whole (100%).
178        InvalidInflationParameters,
179    }
180
181    /// Active inflation configuration parameters.
182    /// They describe current rewards, when inflation needs to be recalculated, etc.
183    #[pallet::storage]
184    #[pallet::whitelist_storage]
185    pub type ActiveInflationConfig<T: Config> = StorageValue<_, InflationConfiguration, ValueQuery>;
186
187    /// Static inflation parameters - used to calculate active inflation configuration at certain points in time.
188    #[pallet::storage]
189    pub type InflationParams<T: Config> = StorageValue<_, InflationParameters, ValueQuery>;
190
191    /// Flag indicating whether on the first possible opportunity, recalculation of the inflation config should be done.
192    #[pallet::storage]
193    #[pallet::whitelist_storage]
194    pub type DoRecalculation<T: Config> = StorageValue<_, EraNumber, OptionQuery>;
195
196    #[pallet::genesis_config]
197    #[derive(DefaultNoBound)]
198    pub struct GenesisConfig<T> {
199        pub params: InflationParameters,
200        #[serde(skip)]
201        pub _config: sp_std::marker::PhantomData<T>,
202    }
203
204    /// This should be executed **AFTER** other pallets that cause issuance to increase have been initialized.
205    #[pallet::genesis_build]
206    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
207        fn build(&self) {
208            assert!(self.params.is_valid());
209
210            let starting_era = 1;
211            let starting_decay_factor = Perquintill::one();
212            let config = Pallet::<T>::recalculate_inflation(starting_era, starting_decay_factor);
213
214            ActiveInflationConfig::<T>::put(config);
215            InflationParams::<T>::put(self.params);
216        }
217    }
218
219    #[pallet::hooks]
220    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
221        fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
222            let mut weight = T::DbWeight::get().reads(1);
223            let mut config = ActiveInflationConfig::<T>::get();
224
225            if config.decay_rate != Perquintill::one() {
226                config.decay_factor = config.decay_factor * config.decay_rate;
227                ActiveInflationConfig::<T>::put(config);
228                weight = weight.saturating_add(T::DbWeight::get().writes(1));
229            }
230
231            Self::payout_block_rewards(&config);
232
233            // Benchmarks won't account for the whitelisted storage access so this needs to be added manually.
234            // DoRecalculation - 1 DB read
235            weight = weight.saturating_add(<T as frame_system::Config>::DbWeight::get().reads(1));
236
237            weight
238        }
239
240        fn on_finalize(_now: BlockNumberFor<T>) {
241            // Recalculation is done at the block right before a new cycle starts.
242            // This is to ensure all the rewards are paid out according to the new inflation configuration from next block.
243            //
244            // If this was done in `on_initialize`, collator & treasury would receive incorrect rewards for that one block.
245            //
246            // This should be done as late as possible, to ensure all operations that modify issuance are done.
247            if let Some(next_era) = DoRecalculation::<T>::get() {
248                let decay_factor = ActiveInflationConfig::<T>::get().decay_factor;
249                let config = Self::recalculate_inflation(next_era, decay_factor);
250                ActiveInflationConfig::<T>::put(config.clone());
251                DoRecalculation::<T>::kill();
252
253                Self::deposit_event(Event::<T>::NewInflationConfiguration { config });
254            }
255
256            // NOTE: weight of the `on_finalize` logic with recalculation has to be covered by the observer notify call.
257        }
258
259        fn integrity_test() {
260            assert!(T::CycleConfiguration::periods_per_cycle() > 0);
261            assert!(T::CycleConfiguration::eras_per_voting_subperiod() > 0);
262            assert!(T::CycleConfiguration::eras_per_build_and_earn_subperiod() > 0);
263            assert!(T::CycleConfiguration::blocks_per_era() > 0);
264        }
265    }
266
267    #[pallet::call]
268    impl<T: Config> Pallet<T> {
269        /// Used to force-set the inflation parameters.
270        /// The parameters must be valid, all parts summing up to one whole (100%), otherwise the call will fail.
271        ///
272        /// Must be called by `root` origin.
273        ///
274        /// Purpose of the call is testing & handling unforeseen circumstances.
275        #[pallet::call_index(0)]
276        #[pallet::weight(T::WeightInfo::force_set_inflation_params())]
277        pub fn force_set_inflation_params(
278            origin: OriginFor<T>,
279            params: InflationParameters,
280        ) -> DispatchResult {
281            ensure_root(origin)?;
282
283            ensure!(params.is_valid(), Error::<T>::InvalidInflationParameters);
284            InflationParams::<T>::put(params);
285
286            Self::deposit_event(Event::<T>::InflationParametersForceChanged);
287
288            Ok(().into())
289        }
290
291        /// Used to force inflation recalculation.
292        /// This is done in the same way as it would be done in an appropriate block, but this call forces it.
293        ///
294        /// Must be called by `root` origin.
295        ///
296        /// Purpose of the call is testing & handling unforeseen circumstances.
297        #[pallet::call_index(1)]
298        #[pallet::weight(T::WeightInfo::force_inflation_recalculation().saturating_add(T::DbWeight::get().writes(1)))]
299        pub fn force_inflation_recalculation(
300            origin: OriginFor<T>,
301            next_era: EraNumber,
302        ) -> DispatchResult {
303            ensure_root(origin)?;
304
305            let decay_factor = ActiveInflationConfig::<T>::get().decay_factor;
306            let config = Self::recalculate_inflation(next_era, decay_factor);
307            ActiveInflationConfig::<T>::put(config.clone());
308
309            Self::deposit_event(Event::<T>::ForcedInflationRecalculation { config });
310
311            Ok(().into())
312        }
313
314        /// Re-adjust the existing inflation configuration using the current inflation parameters.
315        ///
316        /// It might seem similar to forcing the inflation recalculation, but it's not.
317        /// This function adjusts the existing configuration, respecting the `max_emission` value used to calculate the current inflation config.
318        /// (The 'force' approach uses the current total issuance)
319        ///
320        /// This call should be used in case inflation parameters have changed during the cycle, and the configuration should be adjusted now.
321        ///
322        /// NOTE:
323        /// The call will do the best possible approximation of what the calculated max emission was at the moment when last inflation recalculation was done.
324        /// But due to rounding losses, it's not possible to get the exact same value. As a consequence, repeated calls to this function
325        /// might result in changes to the configuration, even though the inflation parameters haven't changed.
326        /// However, since this function isn't supposed to be called often, and changes are minimal, this is acceptable.
327        #[pallet::call_index(2)]
328        #[pallet::weight(T::WeightInfo::force_readjust_config().saturating_add(T::DbWeight::get().writes(1)))]
329        pub fn force_readjust_config(origin: OriginFor<T>) -> DispatchResult {
330            ensure_root(origin)?;
331
332            let config = Self::readjusted_config();
333            ActiveInflationConfig::<T>::put(config.clone());
334
335            Self::deposit_event(Event::<T>::ForcedInflationRecalculation { config });
336
337            Ok(().into())
338        }
339    }
340
341    impl<T: Config> Pallet<T> {
342        /// Payout block rewards to the beneficiaries applying the decay factor.
343        fn payout_block_rewards(config: &InflationConfiguration) {
344            let collator_rewards = config.decay_factor * config.collator_reward_per_block;
345            let treasury_rewards = config.decay_factor * config.treasury_reward_per_block;
346
347            let collator_amount = T::Currency::issue(collator_rewards);
348            let treasury_amount = T::Currency::issue(treasury_rewards);
349
350            T::PayoutPerBlock::collators(collator_amount);
351            T::PayoutPerBlock::treasury(treasury_amount);
352        }
353
354        /// Recalculates the inflation based on the current total issuance & inflation parameters.
355        ///
356        /// Returns the new inflation configuration.
357        pub(crate) fn recalculate_inflation(
358            next_era: EraNumber,
359            decay_factor: Perquintill,
360        ) -> InflationConfiguration {
361            // Calculate max emission based on the current total issuance.
362            let params = InflationParams::<T>::get();
363            let total_issuance = T::Currency::total_issuance();
364            let max_emission = params.max_inflation_rate * total_issuance;
365
366            let recalculation_era =
367                next_era.saturating_add(T::CycleConfiguration::eras_per_cycle());
368
369            Self::new_config(recalculation_era, max_emission, decay_factor)
370        }
371
372        /// Re-adjust the existing inflation configuration using the current inflation parameters.
373        ///
374        /// It might seem similar to forcing the inflation recalculation, but it's not.
375        /// This function adjusts the existing configuration, respecting the `max_emission` value used to calculate the current inflation config.
376        /// (The 'force' approach uses the current total issuance)
377        ///
378        /// This call should be used in case inflation parameters have changed during the cycle, and the configuration should be adjusted now.
379        pub(crate) fn readjusted_config() -> InflationConfiguration {
380            // 1. First calculate the params needed to derive the `max_emission` value used to calculate the current inflation config.
381            let config = ActiveInflationConfig::<T>::get();
382
383            // Simple type conversion.
384            let blocks_per_cycle = Balance::from(T::CycleConfiguration::blocks_per_cycle());
385            let build_and_earn_eras_per_cycle =
386                Balance::from(T::CycleConfiguration::build_and_earn_eras_per_cycle());
387            let periods_per_cycle = Balance::from(T::CycleConfiguration::periods_per_cycle());
388
389            // 2. Calculate reward pool amounts per cycle from the existing inflation configuration.
390            let collator_reward_pool = config
391                .collator_reward_per_block
392                .saturating_mul(blocks_per_cycle);
393
394            let treasury_reward_pool = config
395                .treasury_reward_per_block
396                .saturating_mul(blocks_per_cycle);
397
398            let dapp_reward_pool = config
399                .dapp_reward_pool_per_era
400                .saturating_mul(build_and_earn_eras_per_cycle);
401
402            let base_staker_reward_pool = config
403                .base_staker_reward_pool_per_era
404                .saturating_mul(build_and_earn_eras_per_cycle);
405            let adjustable_staker_reward_pool = config
406                .adjustable_staker_reward_pool_per_era
407                .saturating_mul(build_and_earn_eras_per_cycle);
408
409            let bonus_reward_pool = config
410                .bonus_reward_pool_per_period
411                .saturating_mul(periods_per_cycle);
412
413            // 3. Sum up all values to get the old `max_emission` value.
414            let max_emission = collator_reward_pool
415                .saturating_add(treasury_reward_pool)
416                .saturating_add(dapp_reward_pool)
417                .saturating_add(base_staker_reward_pool)
418                .saturating_add(adjustable_staker_reward_pool)
419                .saturating_add(bonus_reward_pool);
420
421            // 4. Calculate new inflation configuration
422            Self::new_config(config.recalculation_era, max_emission, config.decay_factor)
423        }
424
425        // Calculate new inflation configuration, based on the provided `max_emission`.
426        fn new_config(
427            recalculation_era: EraNumber,
428            max_emission: Balance,
429            decay_factor: Perquintill,
430        ) -> InflationConfiguration {
431            let params = InflationParams::<T>::get();
432
433            // Invalidated parameter, should be cleaned up in the future.
434            // The reason for it's invalidity is because since we've entered the 2nd cycle, it's possible for total
435            // issuance to exceed this cap if unclaimed rewards from previous cycle are claimed.
436            //
437            // In future upgrades, the storage scheme can be updated to completely clean this up.
438            let issuance_safety_cap = Balance::MAX / 1000;
439
440            // 1. Calculate distribution of max emission between different purposes.
441            let treasury_emission = params.treasury_part * max_emission;
442            let collators_emission = params.collators_part * max_emission;
443            let dapps_emission = params.dapps_part * max_emission;
444            let base_stakers_emission = params.base_stakers_part * max_emission;
445            let adjustable_stakers_emission = params.adjustable_stakers_part * max_emission;
446            let bonus_emission = params.bonus_part * max_emission;
447
448            // 2. Calculate concrete rewards per block, era or period
449
450            // 2.0 Convert all 'per cycle' values to the correct type (Balance).
451            // Also include a safety check that none of the values is zero since this would cause a division by zero.
452            // The configuration & integration tests must ensure this never happens, so the following code is just an additional safety measure.
453            //
454            // NOTE: Using `max(1)` to eliminate possibility of division by zero.
455            // These values should never be 0 anyways, but this is just a safety measure.
456            let blocks_per_cycle = Balance::from(T::CycleConfiguration::blocks_per_cycle().max(1));
457            let build_and_earn_eras_per_cycle =
458                Balance::from(T::CycleConfiguration::build_and_earn_eras_per_cycle().max(1));
459            let periods_per_cycle =
460                Balance::from(T::CycleConfiguration::periods_per_cycle().max(1));
461
462            // 2.1. Collator & Treasury rewards per block
463            let collator_reward_per_block = collators_emission.saturating_div(blocks_per_cycle);
464            let treasury_reward_per_block = treasury_emission.saturating_div(blocks_per_cycle);
465
466            // 2.2. dApp reward pool per era
467            let dapp_reward_pool_per_era =
468                dapps_emission.saturating_div(build_and_earn_eras_per_cycle);
469
470            // 2.3. Staking reward pools per era
471            let base_staker_reward_pool_per_era =
472                base_stakers_emission.saturating_div(build_and_earn_eras_per_cycle);
473            let adjustable_staker_reward_pool_per_era =
474                adjustable_stakers_emission.saturating_div(build_and_earn_eras_per_cycle);
475
476            // 2.4. Bonus reward pool per period
477            let bonus_reward_pool_per_period = bonus_emission.saturating_div(periods_per_cycle);
478
479            // 3. Prepare config & do sanity check of its values.
480            let new_inflation_config = InflationConfiguration {
481                recalculation_era,
482                issuance_safety_cap,
483                collator_reward_per_block,
484                treasury_reward_per_block,
485                dapp_reward_pool_per_era,
486                base_staker_reward_pool_per_era,
487                adjustable_staker_reward_pool_per_era,
488                bonus_reward_pool_per_period,
489                ideal_staking_rate: params.ideal_staking_rate,
490                decay_rate: params.decay_rate,
491                decay_factor,
492            };
493            new_inflation_config.sanity_check();
494
495            new_inflation_config
496        }
497    }
498
499    impl<T: Config> DappStakingObserver for Pallet<T> {
500        /// Informs the pallet that the next block will be the first block of a new era.
501        fn block_before_new_era(new_era: EraNumber) -> Weight {
502            let config = ActiveInflationConfig::<T>::get();
503            if config.recalculation_era <= new_era {
504                DoRecalculation::<T>::put(new_era);
505
506                // Need to account for write into a single whitelisted storage item.
507                T::WeightInfo::recalculation().saturating_add(T::DbWeight::get().writes(1))
508            } else {
509                Weight::zero()
510            }
511        }
512    }
513
514    impl<T: Config> StakingRewardHandler<T::AccountId> for Pallet<T> {
515        fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance) {
516            let config = ActiveInflationConfig::<T>::get();
517            let total_issuance = T::Currency::total_issuance();
518
519            // First calculate the adjustable part of the staker reward pool, according to formula:
520            // adjustable_part = max_adjustable_part * min(1, total_staked_percent / ideal_staked_percent)
521            // (These operations are overflow & zero-division safe)
522            let staked_ratio = Perquintill::from_rational(total_value_staked, total_issuance);
523            let adjustment_factor = staked_ratio / config.ideal_staking_rate;
524
525            let adjustable_part = adjustment_factor * config.adjustable_staker_reward_pool_per_era;
526            let staker_reward_pool = config.decay_factor
527                * config
528                    .base_staker_reward_pool_per_era
529                    .saturating_add(adjustable_part);
530            let dapp_reward_pool = config.decay_factor * config.dapp_reward_pool_per_era;
531
532            (staker_reward_pool, dapp_reward_pool)
533        }
534
535        fn bonus_reward_pool() -> Balance {
536            let config = ActiveInflationConfig::<T>::get();
537            config.decay_factor * config.bonus_reward_pool_per_period
538        }
539
540        fn payout_reward(account: &T::AccountId, reward: Balance) -> Result<(), ()> {
541            // This can fail only if the amount is below existential deposit & the account doesn't exist,
542            // or if the account has no provider references.
543            // Another possibility is overflow, but if that happens, we already have a huge problem.
544            //
545            // In both cases, the reward is lost but this can be ignored since it's extremely unlikely
546            // to appear and doesn't bring any real harm.
547            let _ = T::Currency::deposit(account, reward, Precision::Exact);
548            Ok(())
549        }
550    }
551}
552
553/// Configuration of the inflation.
554/// Contains information about rewards, when inflation is recalculated, etc.
555#[derive(
556    Encode,
557    Decode,
558    DecodeWithMemTracking,
559    MaxEncodedLen,
560    Default,
561    Copy,
562    Clone,
563    Debug,
564    PartialEq,
565    Eq,
566    TypeInfo,
567)]
568#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
569pub struct InflationConfiguration {
570    /// Era number at which the inflation configuration must be recalculated, based on the total issuance at that block.
571    #[codec(compact)]
572    pub recalculation_era: EraNumber,
573    /// Maximum amount of issuance we can have during this cycle.
574    #[codec(compact)]
575    pub issuance_safety_cap: Balance,
576    /// Reward for collator who produced the block. Always deposited the collator in full.
577    #[codec(compact)]
578    pub collator_reward_per_block: Balance,
579    /// Part of the inflation going towards the treasury. Always deposited in full.
580    #[codec(compact)]
581    pub treasury_reward_per_block: Balance,
582    /// dApp reward pool per era - based on this the tier rewards are calculated.
583    /// There's no guarantee that this whole amount will be minted & distributed.
584    #[codec(compact)]
585    pub dapp_reward_pool_per_era: Balance,
586    /// Base staker reward pool per era - this is always provided to stakers, regardless of the total value staked.
587    #[codec(compact)]
588    pub base_staker_reward_pool_per_era: Balance,
589    /// Adjustable staker rewards, based on the total value staked.
590    /// This is provided to the stakers according to formula: 'pool * min(1, total_staked / ideal_staked)'.
591    #[codec(compact)]
592    pub adjustable_staker_reward_pool_per_era: Balance,
593    /// Bonus reward pool per period, for eligible stakers.
594    #[codec(compact)]
595    pub bonus_reward_pool_per_period: Balance,
596    /// The ideal staking rate, in respect to total issuance.
597    /// Used to derive exact amount of adjustable staker rewards.
598    #[codec(compact)]
599    pub ideal_staking_rate: Perquintill,
600    /// Per-block decay rate applied to the decay factor.
601    /// A value of `Perquintill::one()` means no decay.
602    #[codec(compact)]
603    pub decay_rate: Perquintill,
604    /// Compounded decay multiplied into rewards when they are actually paid.
605    /// A value of `Perquintill::one()` means no decay.
606    #[codec(compact)]
607    pub decay_factor: Perquintill,
608}
609
610impl InflationConfiguration {
611    /// Sanity check that does rudimentary checks on the configuration and prints warnings if something is unexpected.
612    ///
613    /// There are no strict checks, since the configuration values aren't strictly bounded like those of the parameters.
614    pub fn sanity_check(&self) {
615        if self.collator_reward_per_block.is_zero() {
616            log::warn!("Collator reward per block is zero. If this is not expected, please report this to Astar team.");
617        }
618        if self.treasury_reward_per_block.is_zero() {
619            log::warn!("Treasury reward per block is zero. If this is not expected, please report this to Astar team.");
620        }
621        if self.dapp_reward_pool_per_era.is_zero() {
622            log::warn!("dApp reward pool per era is zero. If this is not expected, please report this to Astar team.");
623        }
624        if self.base_staker_reward_pool_per_era.is_zero() {
625            log::warn!("Base staker reward pool per era is zero.  If this is not expected, please report this to Astar team.");
626        }
627        if self.adjustable_staker_reward_pool_per_era.is_zero() {
628            log::warn!("Adjustable staker reward pool per era is zero.  If this is not expected, please report this to Astar team.");
629        }
630        if self.bonus_reward_pool_per_period.is_zero() {
631            log::warn!("Bonus reward pool per period is zero.  If this is not expected, please report this to Astar team.");
632        }
633    }
634}
635
636/// Inflation parameters.
637///
638/// The parts of the inflation that go towards different purposes must add up to exactly 100%.
639#[derive(
640    Encode,
641    Decode,
642    DecodeWithMemTracking,
643    MaxEncodedLen,
644    Copy,
645    Clone,
646    Debug,
647    PartialEq,
648    Eq,
649    TypeInfo,
650    Serialize,
651    Deserialize,
652)]
653pub struct InflationParameters {
654    /// Maximum possible inflation rate, based on the total issuance at some point in time.
655    /// From this value, all the other inflation parameters are derived.
656    #[codec(compact)]
657    pub max_inflation_rate: Perquintill,
658    /// Portion of the inflation that goes towards the treasury.
659    #[codec(compact)]
660    pub treasury_part: Perquintill,
661    /// Portion of the inflation that goes towards collators.
662    #[codec(compact)]
663    pub collators_part: Perquintill,
664    /// Portion of the inflation that goes towards dApp rewards (tier rewards).
665    #[codec(compact)]
666    pub dapps_part: Perquintill,
667    /// Portion of the inflation that goes towards base staker rewards.
668    #[codec(compact)]
669    pub base_stakers_part: Perquintill,
670    /// Portion of the inflation that can go towards the adjustable staker rewards.
671    /// These rewards are adjusted based on the total value staked.
672    #[codec(compact)]
673    pub adjustable_stakers_part: Perquintill,
674    /// Portion of the inflation that goes towards bonus staker rewards (loyalty rewards).
675    #[codec(compact)]
676    pub bonus_part: Perquintill,
677    /// The ideal staking rate, in respect to total issuance.
678    /// Used to derive exact amount of adjustable staker rewards.
679    #[codec(compact)]
680    pub ideal_staking_rate: Perquintill,
681    /// Per-block decay rate applied to all reward pools and per-block rewards.
682    /// A value of `Perquintill::one()` means no decay.
683    #[codec(compact)]
684    pub decay_rate: Perquintill,
685}
686
687impl InflationParameters {
688    /// `true` if sum of all percentages is `one whole`, `false` otherwise.
689    pub fn is_valid(&self) -> bool {
690        let variables = [
691            &self.treasury_part,
692            &self.collators_part,
693            &self.dapps_part,
694            &self.base_stakers_part,
695            &self.adjustable_stakers_part,
696            &self.bonus_part,
697        ];
698
699        variables
700            .iter()
701            .fold(Some(Perquintill::zero()), |acc, part| {
702                if let Some(acc) = acc {
703                    acc.checked_add(*part)
704                } else {
705                    None
706                }
707            })
708            == Some(Perquintill::one())
709    }
710}
711
712// Default inflation parameters, just to make sure genesis builder is happy
713impl Default for InflationParameters {
714    fn default() -> Self {
715        Self {
716            max_inflation_rate: Perquintill::from_percent(7),
717            treasury_part: Perquintill::from_percent(5),
718            collators_part: Perquintill::from_percent(3),
719            dapps_part: Perquintill::from_percent(20),
720            base_stakers_part: Perquintill::from_percent(25),
721            adjustable_stakers_part: Perquintill::from_percent(35),
722            bonus_part: Perquintill::from_percent(12),
723            ideal_staking_rate: Perquintill::from_percent(50),
724
725            // Use non-default decay rate when benchmarking to measure decay calculation
726            #[cfg(feature = "runtime-benchmarks")]
727            decay_rate: Perquintill::from_percent(99),
728
729            #[cfg(not(feature = "runtime-benchmarks"))]
730            decay_rate: Perquintill::one(),
731        }
732    }
733}
734
735/// Defines functions used to payout the beneficiaries of block rewards
736pub trait PayoutPerBlock<Imbalance> {
737    /// Payout reward to the treasury.
738    fn treasury(reward: Imbalance);
739
740    /// Payout reward to the collator responsible for producing the block.
741    fn collators(reward: Imbalance);
742}