1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
// This file is part of Astar.
// Copyright (C) Stake Technologies Pte.Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later
// Astar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Astar is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Astar. If not, see <http://www.gnu.org/licenses/>.
//! # Inflation Handler Pallet
//!
//! ## Overview
//!
//! This pallet's main responsibility is handling inflation calculation & distribution.
//!
//! Inflation configuration is calculated periodically, according to the inflation parameters.
//! Based on this configuration, rewards are paid out - either per block or on demand.
//!
//! ## Cycles, Periods, Eras
//!
//! At the start of each cycle, the inflation configuration is recalculated.
//!
//! Cycle can be considered as a 'year' in the Astar network.
//! When cycle starts, inflation is calculated according to the total issuance at that point in time.
//! 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.
//!
//! Each cycle consists of one or more `periods`.
//! Periods are integral part of dApp staking protocol, allowing dApps to promote themselves, attract stakers and earn rewards.
//! At the end of each period, all stakes are reset, and dApps need to repeat the process.
//!
//! Each period consists of two subperiods: `Voting` and `Build&Earn`.
//! Length of these subperiods is expressed in eras. An `era` is the core _time unit_ in dApp staking protocol.
//! When an era ends, in `Build&Earn` subperiod, rewards for dApps are calculated & assigned.
//!
//! 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.
//!
//! `Build&Earn` subperiod length is expressed in eras. E.g. if `Build&Earn` subperiod lasts for 5 eras, it means that during that subperiod,
//! 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,
//! with 5 eras per `Build&Earn` subperiod, then the subperiod will end at era 105.
//!
//! `Voting` subperiod always comes before `Build&Earn` subperiod. Its length is also expressed in eras, although it has to be interpreted a bit differently.
//! Even though `Voting` can last for more than 1 era in respect of length, it always takes exactly 1 era.
//! What this means is that if `Voting` lasts for 3 eras, and each era lasts 7200 blocks, then `Voting` will last for 21600 blocks.
//! 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.
//!
//! #### Example
//! * Cycle length: 4 periods
//! * `Voting` length: 10 eras
//! * `Build&Earn` length: 81 eras
//! * Era length: 7200 blocks
//!
//! This would mean that cycle lasts for roughly 364 days (4 * (10 + 81)).
//!
//! ## Recalculation
//!
//! When new cycle begins, inflation configuration is recalculated according to the inflation parameters & total issuance at that point in time.
//! Based on the max inflation rate, rewards for different network actors are calculated.
//!
//! Some rewards are calculated to be paid out per block, while some are per era or per period.
//!
//! ## Rewards
//!
//! ### Collator & Treasury Rewards
//!
//! These are paid out at the beginning of each block & are fixed amounts.
//!
//! ### Staker Rewards
//!
//! Staker rewards are paid out per staker, _on-demand_.
//! However, reward pool for an era is calculated at the end of each era.
//!
//! `era_reward_pool = base_staker_reward_pool_per_era + adjustable_staker_reward_pool_per_era`
//!
//! While the base staker reward pool is fixed, the adjustable part is calculated according to the total value staked & the ideal staking rate.
//!
//! ### dApp Rewards
//!
//! 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.
//! This pallet only provides the total reward pool for all dApps per era.
//!
//! # Interface
//!
//! ## StakingRewardHandler
//!
//! This pallet implements `StakingRewardHandler` trait, which is used by the dApp staking protocol to get reward pools & distribute rewards.
//!
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
use astar_primitives::{
dapp_staking::{
CycleConfiguration, EraNumber, Observer as DappStakingObserver, StakingRewardHandler,
},
Balance,
};
use frame_support::{
pallet_prelude::*,
traits::{
fungible::{Balanced, Credit, Inspect},
tokens::Precision,
},
DefaultNoBound,
};
use frame_system::{ensure_root, pallet_prelude::*};
use serde::{Deserialize, Serialize};
use sp_runtime::{
traits::{CheckedAdd, Zero},
Perquintill,
};
use sp_std::marker::PhantomData;
pub mod weights;
pub use weights::WeightInfo;
#[cfg(any(feature = "runtime-benchmarks"))]
pub mod benchmarking;
pub mod migration;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[frame_support::pallet]
pub mod pallet {
use super::*;
/// The current storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(PhantomData<T>);
// Negative imbalance type of this pallet.
pub(crate) type CreditOf<T> =
Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
#[pallet::config]
pub trait Config: frame_system::Config {
type Currency: Balanced<Self::AccountId, Balance = Balance>;
/// Handler for 'per-block' payouts.
type PayoutPerBlock: PayoutPerBlock<CreditOf<Self>>;
/// Cycle ('year') configuration - covers periods, subperiods, eras & blocks.
type CycleConfiguration: CycleConfiguration;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
/// Inflation parameters have been force changed. This will have effect on the next inflation recalculation.
InflationParametersForceChanged,
/// Inflation configuration has been force changed. This will have an immediate effect from this block.
InflationConfigurationForceChanged { config: InflationConfiguration },
/// Inflation recalculation has been forced.
ForcedInflationRecalculation { config: InflationConfiguration },
/// New inflation configuration has been set.
NewInflationConfiguration { config: InflationConfiguration },
}
#[pallet::error]
pub enum Error<T> {
/// Sum of all parts must be one whole (100%).
InvalidInflationParameters,
}
/// Active inflation configuration parameters.
/// They describe current rewards, when inflation needs to be recalculated, etc.
#[pallet::storage]
#[pallet::whitelist_storage]
pub type ActiveInflationConfig<T: Config> = StorageValue<_, InflationConfiguration, ValueQuery>;
/// Static inflation parameters - used to calculate active inflation configuration at certain points in time.
#[pallet::storage]
pub type InflationParams<T: Config> = StorageValue<_, InflationParameters, ValueQuery>;
/// Flag indicating whether on the first possible opportunity, recalculation of the inflation config should be done.
#[pallet::storage]
#[pallet::whitelist_storage]
pub type DoRecalculation<T: Config> = StorageValue<_, EraNumber, OptionQuery>;
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T> {
pub params: InflationParameters,
#[serde(skip)]
pub _config: sp_std::marker::PhantomData<T>,
}
/// This should be executed **AFTER** other pallets that cause issuance to increase have been initialized.
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
assert!(self.params.is_valid());
let starting_era = 1;
let config = Pallet::<T>::recalculate_inflation(starting_era);
ActiveInflationConfig::<T>::put(config);
InflationParams::<T>::put(self.params);
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
Self::payout_block_rewards();
// Benchmarks won't account for the whitelisted storage access so this needs to be added manually.
//
// ActiveInflationConfig - 1 DB read
// DoRecalculation - 1 DB read
<T as frame_system::Config>::DbWeight::get().reads(2)
}
fn on_finalize(_now: BlockNumberFor<T>) {
// Recalculation is done at the block right before a new cycle starts.
// This is to ensure all the rewards are paid out according to the new inflation configuration from next block.
//
// If this was done in `on_initialize`, collator & treasury would receive incorrect rewards for that one block.
//
// This should be done as late as possible, to ensure all operations that modify issuance are done.
if let Some(next_era) = DoRecalculation::<T>::get() {
let config = Self::recalculate_inflation(next_era);
ActiveInflationConfig::<T>::put(config.clone());
DoRecalculation::<T>::kill();
Self::deposit_event(Event::<T>::NewInflationConfiguration { config });
}
// NOTE: weight of the `on_finalize` logic with recalculation has to be covered by the observer notify call.
}
fn integrity_test() {
assert!(T::CycleConfiguration::periods_per_cycle() > 0);
assert!(T::CycleConfiguration::eras_per_voting_subperiod() > 0);
assert!(T::CycleConfiguration::eras_per_build_and_earn_subperiod() > 0);
assert!(T::CycleConfiguration::blocks_per_era() > 0);
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Used to force-set the inflation parameters.
/// The parameters must be valid, all parts summing up to one whole (100%), otherwise the call will fail.
///
/// Must be called by `root` origin.
///
/// Purpose of the call is testing & handling unforeseen circumstances.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::force_set_inflation_params())]
pub fn force_set_inflation_params(
origin: OriginFor<T>,
params: InflationParameters,
) -> DispatchResult {
ensure_root(origin)?;
ensure!(params.is_valid(), Error::<T>::InvalidInflationParameters);
InflationParams::<T>::put(params);
Self::deposit_event(Event::<T>::InflationParametersForceChanged);
Ok(().into())
}
/// Used to force inflation recalculation.
/// This is done in the same way as it would be done in an appropriate block, but this call forces it.
///
/// Must be called by `root` origin.
///
/// Purpose of the call is testing & handling unforeseen circumstances.
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::force_inflation_recalculation())]
pub fn force_inflation_recalculation(
origin: OriginFor<T>,
next_era: EraNumber,
) -> DispatchResult {
ensure_root(origin)?;
let config = Self::recalculate_inflation(next_era);
ActiveInflationConfig::<T>::put(config.clone());
Self::deposit_event(Event::<T>::ForcedInflationRecalculation { config });
Ok(().into())
}
}
impl<T: Config> Pallet<T> {
/// Payout block rewards to the beneficiaries.
///
/// Return the total amount issued.
fn payout_block_rewards() -> Balance {
let config = ActiveInflationConfig::<T>::get();
let collator_amount = T::Currency::issue(config.collator_reward_per_block);
let treasury_amount = T::Currency::issue(config.treasury_reward_per_block);
T::PayoutPerBlock::collators(collator_amount);
T::PayoutPerBlock::treasury(treasury_amount);
config.collator_reward_per_block + config.treasury_reward_per_block
}
/// Recalculates the inflation based on the total issuance & inflation parameters.
///
/// Returns the new inflation configuration.
pub(crate) fn recalculate_inflation(next_era: EraNumber) -> InflationConfiguration {
let params = InflationParams::<T>::get();
let total_issuance = T::Currency::total_issuance();
// 1. Calculate maximum emission over the period before the next recalculation.
let max_emission = params.max_inflation_rate * total_issuance;
let issuance_safety_cap = total_issuance.saturating_add(max_emission);
// 2. Calculate distribution of max emission between different purposes.
let treasury_emission = params.treasury_part * max_emission;
let collators_emission = params.collators_part * max_emission;
let dapps_emission = params.dapps_part * max_emission;
let base_stakers_emission = params.base_stakers_part * max_emission;
let adjustable_stakers_emission = params.adjustable_stakers_part * max_emission;
let bonus_emission = params.bonus_part * max_emission;
// 3. Calculate concrete rewards per block, era or period
// 3.0 Convert all 'per cycle' values to the correct type (Balance).
// Also include a safety check that none of the values is zero since this would cause a division by zero.
// The configuration & integration tests must ensure this never happens, so the following code is just an additional safety measure.
let blocks_per_cycle = Balance::from(T::CycleConfiguration::blocks_per_cycle().max(1));
let build_and_earn_eras_per_cycle =
Balance::from(T::CycleConfiguration::build_and_earn_eras_per_cycle().max(1));
let periods_per_cycle =
Balance::from(T::CycleConfiguration::periods_per_cycle().max(1));
// 3.1. Collator & Treasury rewards per block
let collator_reward_per_block = collators_emission.saturating_div(blocks_per_cycle);
let treasury_reward_per_block = treasury_emission.saturating_div(blocks_per_cycle);
// 3.2. dApp reward pool per era
let dapp_reward_pool_per_era =
dapps_emission.saturating_div(build_and_earn_eras_per_cycle);
// 3.3. Staking reward pools per era
let base_staker_reward_pool_per_era =
base_stakers_emission.saturating_div(build_and_earn_eras_per_cycle);
let adjustable_staker_reward_pool_per_era =
adjustable_stakers_emission.saturating_div(build_and_earn_eras_per_cycle);
// 3.4. Bonus reward pool per period
let bonus_reward_pool_per_period = bonus_emission.saturating_div(periods_per_cycle);
// 4. Block at which the inflation must be recalculated.
let recalculation_era =
next_era.saturating_add(T::CycleConfiguration::eras_per_cycle());
// 5. Prepare config & do sanity check of its values.
let new_inflation_config = InflationConfiguration {
recalculation_era,
issuance_safety_cap,
collator_reward_per_block,
treasury_reward_per_block,
dapp_reward_pool_per_era,
base_staker_reward_pool_per_era,
adjustable_staker_reward_pool_per_era,
bonus_reward_pool_per_period,
ideal_staking_rate: params.ideal_staking_rate,
};
new_inflation_config.sanity_check();
new_inflation_config
}
/// Check if payout cap limit would be reached after payout.
fn is_payout_cap_limit_exceeded(payout: Balance) -> bool {
let config = ActiveInflationConfig::<T>::get();
let total_issuance = T::Currency::total_issuance();
let new_issuance = total_issuance.saturating_add(payout);
if new_issuance > config.issuance_safety_cap {
log::error!("Issuance cap has been exceeded. Please report this issue ASAP!");
}
// Allow for 1% safety cap overflow, to prevent bad UX for users in case of rounding errors.
// This will be removed in the future once we know everything is working as expected.
let relaxed_issuance_safety_cap = config
.issuance_safety_cap
.saturating_mul(101)
.saturating_div(100);
new_issuance > relaxed_issuance_safety_cap
}
}
impl<T: Config> DappStakingObserver for Pallet<T> {
/// Informs the pallet that the next block will be the first block of a new era.
fn block_before_new_era(new_era: EraNumber) -> Weight {
let config = ActiveInflationConfig::<T>::get();
if config.recalculation_era <= new_era {
DoRecalculation::<T>::put(new_era);
// Need to account for write into a single whitelisted storage item.
T::WeightInfo::recalculation().saturating_add(T::DbWeight::get().writes(1))
} else {
Weight::zero()
}
}
}
impl<T: Config> StakingRewardHandler<T::AccountId> for Pallet<T> {
fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance) {
let config = ActiveInflationConfig::<T>::get();
let total_issuance = T::Currency::total_issuance();
// First calculate the adjustable part of the staker reward pool, according to formula:
// adjustable_part = max_adjustable_part * min(1, total_staked_percent / ideal_staked_percent)
// (These operations are overflow & zero-division safe)
let staked_ratio = Perquintill::from_rational(total_value_staked, total_issuance);
let adjustment_factor = staked_ratio / config.ideal_staking_rate;
let adjustable_part = adjustment_factor * config.adjustable_staker_reward_pool_per_era;
let staker_reward_pool = config
.base_staker_reward_pool_per_era
.saturating_add(adjustable_part);
(staker_reward_pool, config.dapp_reward_pool_per_era)
}
fn bonus_reward_pool() -> Balance {
ActiveInflationConfig::<T>::get().bonus_reward_pool_per_period
}
fn payout_reward(account: &T::AccountId, reward: Balance) -> Result<(), ()> {
// This is a safety measure to prevent excessive minting.
ensure!(!Self::is_payout_cap_limit_exceeded(reward), ());
// This can fail only if the amount is below existential deposit & the account doesn't exist,
// or if the account has no provider references.
// Another possibility is overflow, but if that happens, we already have a huge problem.
//
// In both cases, the reward is lost but this can be ignored since it's extremely unlikely
// to appear and doesn't bring any real harm.
let _ = T::Currency::deposit(account, reward, Precision::Exact);
Ok(())
}
}
}
/// Configuration of the inflation.
/// Contains information about rewards, when inflation is recalculated, etc.
#[derive(Encode, Decode, MaxEncodedLen, Default, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub struct InflationConfiguration {
/// Era number at which the inflation configuration must be recalculated, based on the total issuance at that block.
#[codec(compact)]
pub recalculation_era: EraNumber,
/// Maximum amount of issuance we can have during this cycle.
#[codec(compact)]
pub issuance_safety_cap: Balance,
/// Reward for collator who produced the block. Always deposited the collator in full.
#[codec(compact)]
pub collator_reward_per_block: Balance,
/// Part of the inflation going towards the treasury. Always deposited in full.
#[codec(compact)]
pub treasury_reward_per_block: Balance,
/// dApp reward pool per era - based on this the tier rewards are calculated.
/// There's no guarantee that this whole amount will be minted & distributed.
#[codec(compact)]
pub dapp_reward_pool_per_era: Balance,
/// Base staker reward pool per era - this is always provided to stakers, regardless of the total value staked.
#[codec(compact)]
pub base_staker_reward_pool_per_era: Balance,
/// Adjustable staker rewards, based on the total value staked.
/// This is provided to the stakers according to formula: 'pool * min(1, total_staked / ideal_staked)'.
#[codec(compact)]
pub adjustable_staker_reward_pool_per_era: Balance,
/// Bonus reward pool per period, for loyal stakers.
#[codec(compact)]
pub bonus_reward_pool_per_period: Balance,
/// The ideal staking rate, in respect to total issuance.
/// Used to derive exact amount of adjustable staker rewards.
#[codec(compact)]
pub ideal_staking_rate: Perquintill,
}
impl InflationConfiguration {
/// Sanity check that does rudimentary checks on the configuration and prints warnings if something is unexpected.
///
/// There are no strict checks, since the configuration values aren't strictly bounded like those of the parameters.
pub fn sanity_check(&self) {
if self.collator_reward_per_block.is_zero() {
log::warn!("Collator reward per block is zero. If this is not expected, please report this to Astar team.");
}
if self.treasury_reward_per_block.is_zero() {
log::warn!("Treasury reward per block is zero. If this is not expected, please report this to Astar team.");
}
if self.dapp_reward_pool_per_era.is_zero() {
log::warn!("dApp reward pool per era is zero. If this is not expected, please report this to Astar team.");
}
if self.base_staker_reward_pool_per_era.is_zero() {
log::warn!("Base staker reward pool per era is zero. If this is not expected, please report this to Astar team.");
}
if self.adjustable_staker_reward_pool_per_era.is_zero() {
log::warn!("Adjustable staker reward pool per era is zero. If this is not expected, please report this to Astar team.");
}
if self.bonus_reward_pool_per_period.is_zero() {
log::warn!("Bonus reward pool per period is zero. If this is not expected, please report this to Astar team.");
}
}
}
/// Inflation parameters.
///
/// The parts of the inflation that go towards different purposes must add up to exactly 100%.
#[derive(
Encode,
Decode,
MaxEncodedLen,
Copy,
Clone,
Debug,
PartialEq,
Eq,
TypeInfo,
Serialize,
Deserialize,
)]
pub struct InflationParameters {
/// Maximum possible inflation rate, based on the total issuance at some point in time.
/// From this value, all the other inflation parameters are derived.
#[codec(compact)]
pub max_inflation_rate: Perquintill,
/// Portion of the inflation that goes towards the treasury.
#[codec(compact)]
pub treasury_part: Perquintill,
/// Portion of the inflation that goes towards collators.
#[codec(compact)]
pub collators_part: Perquintill,
/// Portion of the inflation that goes towards dApp rewards (tier rewards).
#[codec(compact)]
pub dapps_part: Perquintill,
/// Portion of the inflation that goes towards base staker rewards.
#[codec(compact)]
pub base_stakers_part: Perquintill,
/// Portion of the inflation that can go towards the adjustable staker rewards.
/// These rewards are adjusted based on the total value staked.
#[codec(compact)]
pub adjustable_stakers_part: Perquintill,
/// Portion of the inflation that goes towards bonus staker rewards (loyalty rewards).
#[codec(compact)]
pub bonus_part: Perquintill,
/// The ideal staking rate, in respect to total issuance.
/// Used to derive exact amount of adjustable staker rewards.
#[codec(compact)]
pub ideal_staking_rate: Perquintill,
}
impl InflationParameters {
/// `true` if sum of all percentages is `one whole`, `false` otherwise.
pub fn is_valid(&self) -> bool {
let variables = [
&self.treasury_part,
&self.collators_part,
&self.dapps_part,
&self.base_stakers_part,
&self.adjustable_stakers_part,
&self.bonus_part,
];
variables
.iter()
.fold(Some(Perquintill::zero()), |acc, part| {
if let Some(acc) = acc {
acc.checked_add(*part)
} else {
None
}
})
== Some(Perquintill::one())
}
}
// Default inflation parameters, just to make sure genesis builder is happy
impl Default for InflationParameters {
fn default() -> Self {
Self {
max_inflation_rate: Perquintill::from_percent(7),
treasury_part: Perquintill::from_percent(5),
collators_part: Perquintill::from_percent(3),
dapps_part: Perquintill::from_percent(20),
base_stakers_part: Perquintill::from_percent(25),
adjustable_stakers_part: Perquintill::from_percent(35),
bonus_part: Perquintill::from_percent(12),
ideal_staking_rate: Perquintill::from_percent(50),
}
}
}
/// Defines functions used to payout the beneficiaries of block rewards
pub trait PayoutPerBlock<Imbalance> {
/// Payout reward to the treasury.
fn treasury(reward: Imbalance);
/// Payout reward to the collator responsible for producing the block.
fn collators(reward: Imbalance);
}