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}