diff --git a/Cargo.lock b/Cargo.lock index 9328f69f90..44c25cb1c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6751,6 +6751,7 @@ dependencies = [ name = "pallet-block-rewards" version = "0.1.0" dependencies = [ + "cfg-mocks", "cfg-primitives", "cfg-traits", "cfg-types", diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 36384f994f..b263767e7b 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -47,7 +47,10 @@ use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::{ChainType, Properties}; use serde::{Deserialize, Serialize}; use sp_core::{crypto::UncheckedInto, sr25519, Encode, Pair, Public, H160}; -use sp_runtime::traits::{IdentifyAccount, Verify}; +use sp_runtime::{ + traits::{IdentifyAccount, Verify}, + FixedPointNumber, +}; use xcm::{ latest::MultiLocation, prelude::{GeneralIndex, GeneralKey, PalletInstance, Parachain, X2, X3}, @@ -62,6 +65,7 @@ pub type DevelopmentChainSpec = use altair_runtime::AltairPrecompiles; use centrifuge_runtime::CentrifugePrecompiles; +use cfg_types::fixed_point::Rate; use development_runtime::DevelopmentPrecompiles; /// Helper function to generate a crypto pair from seed @@ -661,7 +665,11 @@ fn centrifuge_genesis( .map(|(acc, _)| acc) .collect(), collator_reward: 8_325 * MILLI_CFG, - total_reward: 10_048 * CFG, + treasury_inflation_rate: Rate::saturating_from_rational(3, 100), + last_update: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!") + .as_secs(), }, block_rewards_base: Default::default(), base_fee: Default::default(), @@ -759,7 +767,11 @@ fn altair_genesis( .map(|(acc, _)| acc) .collect(), collator_reward: 98_630 * MILLI_AIR, - total_reward: 98_630 * MILLI_AIR * 100, + treasury_inflation_rate: Rate::saturating_from_rational(3, 100), + last_update: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!") + .as_secs(), }, block_rewards_base: Default::default(), collator_allowlist: Default::default(), @@ -945,7 +957,11 @@ fn development_genesis( .map(|(acc, _)| acc) .collect(), collator_reward: 8_325 * MILLI_CFG, - total_reward: 10_048 * CFG, + treasury_inflation_rate: Rate::saturating_from_rational(3, 100), + last_update: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!") + .as_secs(), }, base_fee: Default::default(), evm_chain_id: development_runtime::EVMChainIdConfig { diff --git a/pallets/block-rewards/Cargo.toml b/pallets/block-rewards/Cargo.toml index ed6c1be7f3..769c531ce8 100644 --- a/pallets/block-rewards/Cargo.toml +++ b/pallets/block-rewards/Cargo.toml @@ -32,6 +32,7 @@ sp-std = { workspace = true } frame-benchmarking = { workspace = true, optional = true } [dev-dependencies] +cfg-mocks = { workspace = true, default-features = true } orml-tokens = { workspace = true, default-features = true } orml-traits = { workspace = true, default-features = true } pallet-balances = { workspace = true, default-features = true } diff --git a/pallets/block-rewards/src/benchmarking.rs b/pallets/block-rewards/src/benchmarking.rs index e1b750dce3..b846749d3d 100644 --- a/pallets/block-rewards/src/benchmarking.rs +++ b/pallets/block-rewards/src/benchmarking.rs @@ -1,6 +1,6 @@ use cfg_primitives::CFG; use cfg_types::tokens::CurrencyId; -use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller}; +use frame_benchmarking::v2::*; use frame_support::{ assert_ok, traits::{fungibles::Inspect, Currency as CurrencyT}, @@ -14,56 +14,85 @@ use crate::{pallet::Config, Pallet as BlockRewards}; const REWARD: u128 = 1 * CFG; const SEED: u32 = 0; -benchmarks! { - where_clause { - where +#[benchmarks( +where T::Balance: From, T::BlockNumber: From + One, T::Weight: From, - ::Currency: frame_support::traits::fungibles::Inspect + CurrencyT, + ::Tokens: Inspect + CurrencyT, ::CurrencyId: From, - } +)] +mod benchmarks { + use super::*; - claim_reward { - let caller = whitelisted_caller(); - let beneficiary: T::AccountId = account("collator", 0, SEED); + #[benchmark] + fn claim_reward() -> Result<(), BenchmarkError> { + let caller: T::AccountId = account("caller", 0, SEED); + let beneficiary: T::AccountId = account("collator", 0, SEED); assert_ok!(BlockRewards::::do_init_collator(&beneficiary)); - assert_ok!(T::Rewards::reward_group(T::StakeGroupId::get(), REWARD.into())); + assert_ok!(T::Rewards::reward_group( + T::StakeGroupId::get(), + REWARD.into() + )); assert!(T::Rewards::is_ready(T::StakeGroupId::get())); assert!( - !T::Rewards::compute_reward( - T::StakeCurrencyId::get(), - &beneficiary, - ).unwrap().is_zero() + !T::Rewards::compute_reward(T::StakeCurrencyId::get(), &beneficiary,) + .unwrap() + .is_zero() + ); + let before = + >::balance(CurrencyId::Native.into(), &beneficiary); + + #[extrinsic_call] + claim_reward(RawOrigin::Signed(caller), beneficiary.clone()); + + let num_collators: u128 = BlockRewards::::next_session_changes() + .collator_count + .unwrap_or(BlockRewards::::active_session_data().collator_count) + .into(); + // Does not get entire reward since another collator is auto-staked via genesis + // config + assert_eq!( + >::balance(CurrencyId::Native.into(), &beneficiary) + .saturating_sub(before), + (REWARD / (num_collators + 1)).into() ); - let before = ::Currency::balance(CurrencyId::Native.into(), &beneficiary); - - }: _(RawOrigin::Signed(caller), beneficiary.clone()) - verify { - let num_collators: u128 = BlockRewards::::next_session_changes().collator_count.unwrap_or( - BlockRewards::::active_session_data().collator_count - ).into(); - // Does not get entire reward since another collator is auto-staked via genesis config - assert_eq!(::Currency::balance(CurrencyId::Native.into(), &beneficiary).saturating_sub(before), (REWARD / (num_collators + 1)).into()); + + Ok(()) } - set_collator_reward { - assert_ok!(BlockRewards::::set_total_reward(RawOrigin::Root.into(), u128::MAX.into())); - }: _(RawOrigin::Root, REWARD.into()) - verify { - assert_eq!(BlockRewards::::next_session_changes().collator_reward, Some(REWARD.into())); + #[benchmark] + fn set_collator_reward_per_session() -> Result<(), BenchmarkError> { + #[extrinsic_call] + set_collator_reward_per_session(RawOrigin::Root, REWARD.into()); + + assert_eq!( + BlockRewards::::next_session_changes().collator_reward, + Some(REWARD.into()) + ); + + Ok(()) } - set_total_reward { - }: _(RawOrigin::Root, u128::MAX.into()) - verify { - assert_eq!(BlockRewards::::next_session_changes().total_reward, Some(u128::MAX.into())); + #[benchmark] + fn set_annual_treasury_inflation_rate() -> Result<(), BenchmarkError> { + let rate = T::Rate::saturating_from_rational(1, 2); + + #[extrinsic_call] + set_annual_treasury_inflation_rate(RawOrigin::Root, rate); + + assert_eq!( + BlockRewards::::next_session_changes().treasury_inflation_rate, + Some(rate) + ); + + Ok(()) } -} -impl_benchmark_test_suite!( - BlockRewards, - crate::mock::ExtBuilder::default().build(), - crate::mock::Test, -); + impl_benchmark_test_suite!( + BlockRewards, + crate::mock::ExtBuilder::default().build(), + crate::mock::Test, + ); +} diff --git a/pallets/block-rewards/src/lib.rs b/pallets/block-rewards/src/lib.rs index 1e8c4912dd..acd9516c9f 100644 --- a/pallets/block-rewards/src/lib.rs +++ b/pallets/block-rewards/src/lib.rs @@ -13,10 +13,10 @@ //! # BlockRewards Pallet //! //! The BlockRewards pallet provides functionality for distributing rewards to -//! different accounts with different currencies. -//! The distribution happens when an session (a constant time interval) -//! finalizes. Users cannot stake manually as their collator membership is -//! syncronized via a provider. +//! different accounts with different currencies as well as configuring an +//! annual treasury inflation. The distribution happens when a session (a +//! constant time interval) finalizes. Users cannot stake manually as their +//! collator membership is synchronized via a provider. //! Thus, when new collators join, they will automatically be staked and //! vice-versa when collators leave, they are unstaked. //! @@ -24,8 +24,8 @@ //! //! - Claiming the reward given for a staked currency. The reward will be the //! native network's token. -//! - Admin methods to configure the reward amount for collators and an optional -//! beneficiary. +//! - Admin methods to configure the reward amount for collators and the annual +//! treasury inflation. #![cfg_attr(not(feature = "std"), no_std)] #[cfg(test)] @@ -43,23 +43,26 @@ mod benchmarking; use cfg_traits::{ self, rewards::{AccountRewards, CurrencyGroupChange, GroupRewards}, + Seconds, TimeAsSecs, }; +use cfg_types::fixed_point::FixedPointNumberExtension; use frame_support::{ pallet_prelude::*, storage::transactional, traits::{ + fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate}, fungibles::Mutate, tokens::{Balance, Fortitude, Precision}, - Currency as CurrencyT, OnUnbalanced, OneSessionHandler, + OneSessionHandler, }, - DefaultNoBound, + DefaultNoBound, PalletId, }; use frame_system::pallet_prelude::*; use num_traits::sign::Unsigned; pub use pallet::*; use sp_runtime::{ - traits::{EnsureAdd, EnsureMul, EnsureSub, Zero}, - FixedPointOperand, SaturatedConversion, Saturating, + traits::{AccountIdConversion, EnsureAdd, EnsureMul, Zero}, + FixedPointNumber, FixedPointOperand, SaturatedConversion, Saturating, }; use sp_std::{mem, vec::Vec}; pub use weights::WeightInfo; @@ -79,20 +82,22 @@ pub struct CollatorChanges { pub struct SessionData { /// Amount of rewards per session for a single collator. pub(crate) collator_reward: T::Balance, - /// Total amount of rewards per session - /// NOTE: Is ensured to be at least collator_reward * collator_count. - pub(crate) total_reward: T::Balance, /// Number of current collators. /// NOTE: Updated automatically and thus not adjustable via extrinsic. pub collator_count: u32, + /// The annual treasury inflation rate + pub(crate) treasury_inflation_rate: T::Rate, + /// The timestamp of the last update used for inflation proration + pub(crate) last_update: Seconds, } impl Default for SessionData { fn default() -> Self { Self { - collator_reward: T::Balance::zero(), - total_reward: T::Balance::zero(), collator_count: 0, + collator_reward: T::Balance::zero(), + treasury_inflation_rate: T::Rate::zero(), + last_update: Seconds::zero(), } } } @@ -105,20 +110,17 @@ impl Default for SessionData { pub struct SessionChanges { pub collators: CollatorChanges, pub collator_count: Option, - collator_reward: Option, - total_reward: Option, + pub collator_reward: Option, + treasury_inflation_rate: Option, + last_update: Seconds, } -pub(crate) type NegativeImbalanceOf = <::Currency as CurrencyT< - ::AccountId, ->>::NegativeImbalance; - #[frame_support::pallet] pub mod pallet { use super::*; - pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::config] pub trait Config: frame_system::Config { @@ -129,11 +131,7 @@ pub mod pallet { type AdminOrigin: EnsureOrigin; /// Type used to handle balances. - type Balance: Balance - + MaxEncodedLen - + FixedPointOperand - + Into<<::Currency as CurrencyT>::Balance> - + MaybeSerializeDeserialize; + type Balance: Balance + MaxEncodedLen + FixedPointOperand + MaybeSerializeDeserialize; #[pallet::constant] type ExistentialDeposit: Get; @@ -150,17 +148,12 @@ pub mod pallet { > + CurrencyGroupChange::CurrencyId>; /// The type used to handle currency minting and burning for collators. - type Currency: Mutate::CurrencyId, Balance = Self::Balance> - + CurrencyT; + type Tokens: Mutate::CurrencyId, Balance = Self::Balance> + + FungibleMutate + + FungibleInspect; /// The currency type of the artificial block rewards currency. - type CurrencyId: Parameter - + Member - + Copy - + MaybeSerializeDeserialize - + Ord - + TypeInfo - + MaxEncodedLen; + type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord + MaxEncodedLen; /// The identifier of the artificial block rewards currency which is /// minted and burned for collators. @@ -176,19 +169,11 @@ pub mod pallet { #[pallet::constant] type StakeGroupId: Get; - /// Max number of changes of the same type enqueued to apply in the next - /// session. Max calls to [`Pallet::set_collator_reward()`] or to - /// [`Pallet::set_total_reward()`] with the same id. - #[pallet::constant] - type MaxChangesPerSession: Get + TypeInfo + sp_std::fmt::Debug + Clone + PartialEq; - #[pallet::constant] type MaxCollators: Get + TypeInfo + sp_std::fmt::Debug + Clone + PartialEq; - /// Target of receiving non-collator-rewards. - /// NOTE: If set to none, collators are the only group receiving - /// rewards. - type Beneficiary: OnUnbalanced>; + /// Treasury pallet + type TreasuryPalletId: Get; /// The identifier type for an authority. type AuthorityId: Member @@ -197,7 +182,17 @@ pub mod pallet { + MaybeSerializeDeserialize + MaxEncodedLen; - /// Information of runtime weightsk + /// The inflation rate type + type Rate: Parameter + + Member + + FixedPointNumberExtension + + MaybeSerializeDeserialize + + MaxEncodedLen; + + /// The source of truth for the current time in seconds + type Time: TimeAsSecs; + + /// Information of runtime weights type WeightInfo: WeightInfo; } @@ -221,7 +216,7 @@ pub mod pallet { pub enum Event { NewSession { collator_reward: T::Balance, - total_reward: T::Balance, + treasury_inflation_rate: T::Rate, last_changes: SessionChanges, }, SessionAdvancementFailed { @@ -230,18 +225,14 @@ pub mod pallet { } #[pallet::error] - pub enum Error { - /// Limit of max calls with same id to [`Pallet::set_collator_reward()`] - /// or [`Pallet::set_total_reward()`] reached. - MaxChangesPerSessionReached, - InsufficientTotalReward, - } + pub enum Error {} #[pallet::genesis_config] pub struct GenesisConfig { pub collators: Vec, pub collator_reward: T::Balance, - pub total_reward: T::Balance, + pub treasury_inflation_rate: T::Rate, + pub last_update: Seconds, } #[cfg(feature = "std")] @@ -250,7 +241,8 @@ pub mod pallet { GenesisConfig { collators: Default::default(), collator_reward: Default::default(), - total_reward: Default::default(), + treasury_inflation_rate: Default::default(), + last_update: Default::default(), } } } @@ -265,7 +257,8 @@ pub mod pallet { ActiveSessionData::::mutate(|session_data| { session_data.collator_count = self.collators.len().saturated_into(); session_data.collator_reward = self.collator_reward; - session_data.total_reward = self.total_reward; + session_data.treasury_inflation_rate = self.treasury_inflation_rate; + session_data.last_update = self.last_update; }); // Enables rewards already in genesis session. @@ -292,62 +285,34 @@ pub mod pallet { /// next sessions. Current session is not affected by this call. #[pallet::weight(T::WeightInfo::set_collator_reward())] #[pallet::call_index(1)] - pub fn set_collator_reward( + pub fn set_collator_reward_per_session( origin: OriginFor, collator_reward_per_session: T::Balance, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - NextSessionChanges::::try_mutate(|changes| { - let current = ActiveSessionData::::get(); - let total_collator_rewards = collator_reward_per_session.saturating_mul( - changes - .collator_count - .unwrap_or(current.collator_count) - .into(), - ); - let total_rewards = changes.total_reward.unwrap_or(current.total_reward); - ensure!( - total_rewards >= total_collator_rewards, - Error::::InsufficientTotalReward - ); - - changes.collator_reward = Some(collator_reward_per_session); - Ok(()) - }) + NextSessionChanges::::mutate(|c| { + c.collator_reward = Some(collator_reward_per_session); + }); + + Ok(()) } - /// Admin method to set the total reward distribution for the next + /// Admin method to set the treasury inflation rate for the next /// sessions. Current session is not affected by this call. - /// - /// Throws if total_reward < collator_reward * collator_count. #[pallet::weight(T::WeightInfo::set_total_reward())] #[pallet::call_index(2)] - pub fn set_total_reward( + pub fn set_annual_treasury_inflation_rate( origin: OriginFor, - total_reward_per_session: T::Balance, + treasury_inflation_rate: T::Rate, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - NextSessionChanges::::try_mutate(|changes| { - let current = ActiveSessionData::::get(); - let total_collator_rewards = changes - .collator_reward - .unwrap_or(current.collator_reward) - .saturating_mul( - changes - .collator_count - .unwrap_or(current.collator_count) - .into(), - ); - ensure!( - total_reward_per_session >= total_collator_rewards, - Error::::InsufficientTotalReward - ); + NextSessionChanges::::mutate(|c| { + c.treasury_inflation_rate = Some(treasury_inflation_rate); + }); - changes.total_reward = Some(total_reward_per_session); - Ok(()) - }) + Ok(()) } } } @@ -361,10 +326,10 @@ impl Pallet { /// * deposit_stake (4 reads, 4 writes): Currency, Group, StakeAccount, /// Account pub(crate) fn do_init_collator(who: &T::AccountId) -> DispatchResult { - T::Currency::mint_into( + >::mint_into( T::StakeCurrencyId::get(), who, - T::StakeAmount::get() + T::ExistentialDeposit::get(), + T::StakeAmount::get().saturating_add(T::ExistentialDeposit::get()), )?; T::Rewards::deposit_stake(T::StakeCurrencyId::get(), who, T::StakeAmount::get()) } @@ -379,9 +344,9 @@ impl Pallet { // would get killed and down the line our orml-tokens prevents // that. // - // I.e. this means stake curreny issuance will grow over time if many + // I.e. this means stake currency issuance will grow over time if many // collators leave and join. - T::Currency::burn_from( + >::burn_from( T::StakeCurrencyId::get(), who, amount, @@ -391,6 +356,20 @@ impl Pallet { .map(|_| ()) } + /// Calculates the inflation proration based on the annual configuration and + /// the session duration in seconds + pub(crate) fn calculate_epoch_treasury_inflation( + annual_inflation_rate: T::Rate, + last_update: Seconds, + ) -> T::Balance { + let total_issuance = >::total_issuance(); + let session_duration = T::Time::now().saturating_sub(last_update); + let inflation_proration = + cfg_types::pools::saturated_rate_proration(annual_inflation_rate, session_duration); + + inflation_proration.saturating_mul_int(total_issuance) + } + /// Apply session changes and distribute rewards. /// /// NOTE: Noop if any call fails. @@ -404,18 +383,25 @@ impl Pallet { // Reward collator group of last session let total_collator_reward = session_data .collator_reward - .ensure_mul(session_data.collator_count.into())? - .min(session_data.total_reward); + .ensure_mul(session_data.collator_count.into())?; T::Rewards::reward_group(T::StakeGroupId::get(), total_collator_reward)?; - // Handle remaining reward - let remaining = session_data - .total_reward - .ensure_sub(total_collator_reward)?; - if !remaining.is_zero() { - let reward = T::Currency::issue(remaining.into()); - // If configured, assigns reward to Beneficiary, else automatically drops it - T::Beneficiary::on_unbalanced(reward); + // Handle treasury inflation + let treasury_inflation = Self::calculate_epoch_treasury_inflation( + session_data.treasury_inflation_rate, + session_data.last_update, + ); + if !treasury_inflation.is_zero() { + let _ = >::mint_into( + &T::TreasuryPalletId::get().into_account_truncating(), + treasury_inflation, + ) + .map_err(|e| { + log::error!( + "Failed to mint treasury inflation for session due to error {:?}", + e + ) + }); } num_joining = changes.collators.inc.len().saturated_into(); @@ -433,15 +419,17 @@ impl Pallet { session_data.collator_reward = changes .collator_reward .unwrap_or(session_data.collator_reward); - session_data.total_reward = - changes.total_reward.unwrap_or(session_data.total_reward); + session_data.treasury_inflation_rate = changes + .treasury_inflation_rate + .unwrap_or(session_data.treasury_inflation_rate); session_data.collator_count = changes .collator_count .unwrap_or(session_data.collator_count); + session_data.last_update = T::Time::now(); Self::deposit_event(Event::NewSession { - total_reward: session_data.total_reward, collator_reward: session_data.collator_reward, + treasury_inflation_rate: session_data.treasury_inflation_rate, last_changes: mem::take(changes), }); diff --git a/pallets/block-rewards/src/migrations.rs b/pallets/block-rewards/src/migrations.rs index 3da2a4601d..de7083b42a 100644 --- a/pallets/block-rewards/src/migrations.rs +++ b/pallets/block-rewards/src/migrations.rs @@ -10,7 +10,7 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use cfg_traits::rewards::CurrencyGroupChange; +use cfg_traits::TimeAsSecs; use frame_support::{ dispatch::GetStorageVersion, inherent::Vec, @@ -18,136 +18,279 @@ use frame_support::{ traits::{Get, OnRuntimeUpgrade}, }; #[cfg(feature = "try-runtime")] -use sp_runtime::DispatchError; -use sp_runtime::{BoundedVec, SaturatedConversion}; -use sp_std::marker::PhantomData; +use num_traits::Zero; +use parity_scale_codec::{Decode, Encode}; +use sp_runtime::FixedPointNumber; #[cfg(feature = "try-runtime")] -use { - cfg_traits::rewards::AccountRewards, - num_traits::Zero, - parity_scale_codec::{Decode, Encode}, -}; +use sp_runtime::TryRuntimeError; +use sp_std::marker::PhantomData; -use crate::{ActiveSessionData, Config, Pallet, SessionData}; - -pub struct InitBlockRewards( - PhantomData<(T, CollatorReward, TotalReward)>, -); - -fn get_collators() -> Vec { - let candidates = BoundedVec::< - T::AccountId, - ::MaxCandidates, - >::truncate_from( - pallet_collator_selection::Pallet::::candidates() - .into_iter() - .map(|c| c.who) - .collect(), - ); - pallet_collator_selection::Pallet::::assemble_collators(candidates) +use crate::{pallet, Config, Pallet, SessionData}; + +fn inflation_rate(percent: u32) -> T::Rate { + T::Rate::saturating_from_rational(percent, 100) } -impl OnRuntimeUpgrade - for InitBlockRewards -where - T: frame_system::Config + Config + pallet_collator_selection::Config, - ::Balance: From, - CollatorReward: Get, - TotalReward: Get, -{ +pub mod init { #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, DispatchError> { - assert_eq!( - Pallet::::on_chain_storage_version(), - StorageVersion::new(0), - "On-chain storage version should be 0 (default)" - ); - let collators = get_collators::(); - assert!(!collators.is_empty()); + use cfg_traits::rewards::AccountRewards; + use cfg_traits::rewards::CurrencyGroupChange; + use sp_runtime::{BoundedVec, SaturatedConversion}; - assert!(!CollatorReward::get().is_zero()); - assert!( - TotalReward::get() - >= CollatorReward::get().saturating_mul(collators.len().saturated_into()) - ); + use super::*; - log::info!("💰 BlockRewards: Pre migration checks successful"); + const LOG_PREFIX: &str = "InitBlockRewards"; + pub struct InitBlockRewards( + PhantomData<(T, CollatorReward, AnnualTreasuryInflationPercent)>, + ); - Ok(collators.encode()) + fn get_collators() -> Vec { + let candidates = BoundedVec::< + T::AccountId, + ::MaxCandidates, + >::truncate_from( + pallet_collator_selection::Pallet::::candidates() + .into_iter() + .map(|c| c.who) + .collect(), + ); + pallet_collator_selection::Pallet::::assemble_collators(candidates) } - // Weight: 2 + collator_count reads and writes - fn on_runtime_upgrade() -> frame_support::weights::Weight { - if Pallet::::on_chain_storage_version() == StorageVersion::new(0) { - log::info!("💰 BlockRewards: Initiating migration"); - let mut weight: Weight = Weight::zero(); - + impl OnRuntimeUpgrade + for InitBlockRewards + where + T: frame_system::Config + Config + pallet_collator_selection::Config, + ::Balance: From, + CollatorReward: Get<::Balance>, + AnnualTreasuryInflationPercent: Get, + { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + assert_eq!( + Pallet::::on_chain_storage_version(), + StorageVersion::new(0), + "On-chain storage version should be 0 (default)" + ); let collators = get_collators::(); - weight.saturating_accrue(T::DbWeight::get().reads(2)); - - ::Rewards::attach_currency( - ::StakeCurrencyId::get(), - ::StakeGroupId::get(), - ) - .map_err(|e| log::error!("Failed to attach currency to collator group: {:?}", e)) - .ok(); - - ActiveSessionData::::set(SessionData:: { - collator_reward: CollatorReward::get().into(), - total_reward: TotalReward::get().into(), - collator_count: collators.len().saturated_into(), - }); - weight.saturating_accrue(T::DbWeight::get().writes(1)); + assert!(!collators.is_empty()); - for collator in collators.iter() { - // NOTE: Benching not required as num of collators <= 10. - Pallet::::do_init_collator(collator) - .map_err(|e| { - log::error!("Failed to init genesis collators for rewards: {:?}", e); - }) - .ok(); - weight.saturating_accrue(T::DbWeight::get().reads_writes(6, 6)); + assert!(!CollatorReward::get().is_zero()); + + log::info!("{LOG_PREFIX} Pre migration checks successful"); + + Ok(collators.encode()) + } + + // Weight: 2 + collator_count reads and writes + fn on_runtime_upgrade() -> Weight { + if Pallet::::on_chain_storage_version() == StorageVersion::new(0) { + log::info!("{LOG_PREFIX} Initiating migration"); + let mut weight: Weight = Weight::zero(); + + let collators = get_collators::(); + weight.saturating_accrue(T::DbWeight::get().reads(2)); + + ::Rewards::attach_currency( + ::StakeCurrencyId::get(), + ::StakeGroupId::get(), + ) + .map_err(|e| log::error!("Failed to attach currency to collator group: {:?}", e)) + .ok(); + + pallet::ActiveSessionData::::set(SessionData:: { + collator_count: collators.len().saturated_into(), + collator_reward: CollatorReward::get(), + treasury_inflation_rate: inflation_rate::( + AnnualTreasuryInflationPercent::get(), + ), + last_update: T::Time::now(), + }); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + for collator in collators.iter() { + // NOTE: Benching not required as num of collators <= 10. + Pallet::::do_init_collator(collator) + .map_err(|e| { + log::error!("Failed to init genesis collators for rewards: {:?}", e); + }) + .ok(); + weight.saturating_accrue(T::DbWeight::get().reads_writes(6, 6)); + } + Pallet::::current_storage_version().put::>(); + weight.saturating_add(T::DbWeight::get().writes(1)) + } else { + // wrong storage version + log::info!( + "{LOG_PREFIX} Migration did not execute. This probably should be removed" + ); + T::DbWeight::get().reads_writes(1, 0) } - Pallet::::current_storage_version().put::>(); - weight.saturating_add(T::DbWeight::get().writes(1)) - } else { - // wrong storage version - log::info!( - "💰 BlockRewards: Migration did not execute. This probably should be removed" + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(pre_state: Vec) -> Result<(), TryRuntimeError> { + assert_eq!( + Pallet::::on_chain_storage_version(), + Pallet::::current_storage_version(), + "On-chain storage version should be updated" + ); + let collators: Vec = Decode::decode(&mut pre_state.as_slice()) + .expect("pre_upgrade provides a valid state; qed"); + + assert_eq!( + Pallet::::active_session_data(), + SessionData:: { + collator_count: collators.len().saturated_into(), + collator_reward: CollatorReward::get(), + treasury_inflation_rate: inflation_rate::( + AnnualTreasuryInflationPercent::get() + ), + last_update: T::Time::now(), + } ); - T::DbWeight::get().reads_writes(1, 0) + + for collator in collators.iter() { + assert!(!::Rewards::account_stake( + ::StakeCurrencyId::get(), + collator, + ) + .is_zero()) + } + + log::info!("{LOG_PREFIX} Post migration checks successful"); + + Ok(()) } } +} - #[cfg(feature = "try-runtime")] - fn post_upgrade(pre_state: Vec) -> Result<(), DispatchError> { - assert_eq!( - Pallet::::on_chain_storage_version(), - StorageVersion::new(1), - "On-chain storage version should be updated" - ); - let collators: Vec = Decode::decode(&mut pre_state.as_slice()) - .expect("pre_ugprade provides a valid state; qed"); - - assert_eq!( - Pallet::::active_session_data(), - SessionData:: { - collator_reward: CollatorReward::get().into(), - total_reward: TotalReward::get().into(), - collator_count: collators.len().saturated_into(), +pub mod v2 { + use frame_support::{ + pallet_prelude::ValueQuery, storage_alias, DefaultNoBound, RuntimeDebugNoBound, + }; + use parity_scale_codec::MaxEncodedLen; + use scale_info::TypeInfo; + + use super::*; + use crate::{CollatorChanges, SessionChanges}; + + const LOG_PREFIX: &str = "RelativeTreasuryInflation"; + + #[derive( + Encode, Decode, TypeInfo, DefaultNoBound, MaxEncodedLen, PartialEq, Eq, RuntimeDebugNoBound, + )] + #[scale_info(skip_type_params(T))] + struct OldSessionData { + pub collator_reward: T::Balance, + pub total_reward: T::Balance, + pub collator_count: u32, + } + + #[derive( + PartialEq, + Clone, + DefaultNoBound, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, + RuntimeDebugNoBound, + )] + #[scale_info(skip_type_params(T))] + struct OldSessionChanges { + pub collators: CollatorChanges, + pub collator_count: Option, + pub collator_reward: Option, + pub total_reward: Option, + } + + #[storage_alias] + type ActiveSessionData = StorageValue, OldSessionData, ValueQuery>; + #[storage_alias] + type NextSessionChanges = StorageValue, OldSessionChanges, ValueQuery>; + + pub struct RelativeTreasuryInflationMigration( + PhantomData<(T, InflationRate)>, + ); + + impl OnRuntimeUpgrade + for RelativeTreasuryInflationMigration + where + T: Config, + InflationPercentage: Get, + { + fn on_runtime_upgrade() -> Weight { + if Pallet::::on_chain_storage_version() == StorageVersion::new(1) { + let active = ActiveSessionData::::take(); + let next = NextSessionChanges::::take(); + + pallet::ActiveSessionData::::put(SessionData { + collator_reward: active.collator_reward, + collator_count: active.collator_count, + treasury_inflation_rate: inflation_rate::(InflationPercentage::get()), + last_update: T::Time::now(), + }); + log::info!("{LOG_PREFIX} Translated ActiveSessionData"); + + pallet::NextSessionChanges::::put(SessionChanges { + collators: next.collators, + collator_count: next.collator_count, + collator_reward: next.collator_reward, + treasury_inflation_rate: Some(inflation_rate::(InflationPercentage::get())), + last_update: T::Time::now(), + }); + log::info!("{LOG_PREFIX} Translated NextSessionChanges"); + Pallet::::current_storage_version().put::>(); + + T::DbWeight::get().reads_writes(1, 5) + } else { + log::info!("{LOG_PREFIX} BlockRewards pallet already on version 2, migration can be removed"); + T::DbWeight::get().reads(1) } - ); + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + assert_eq!( + Pallet::::on_chain_storage_version(), + StorageVersion::new(1), + ); + assert!( + Pallet::::on_chain_storage_version() < Pallet::::current_storage_version() + ); + + let active = ActiveSessionData::::get(); + let next = NextSessionChanges::::get(); - for collator in collators.iter() { - assert!(!::Rewards::account_stake( - ::StakeCurrencyId::get(), - collator, - ) - .is_zero()) + log::info!("{LOG_PREFIX} PRE UPGRADE: Finished"); + + Ok((active, next).encode()) } - log::info!("💰 BlockRewards: Post migration checks successful"); + #[cfg(feature = "try-runtime")] + fn post_upgrade(pre_state: Vec) -> Result<(), TryRuntimeError> { + let (old_active, old_next): (OldSessionData, OldSessionChanges) = + Decode::decode(&mut pre_state.as_slice()).expect("Pre state valid; qed"); + let active = pallet::ActiveSessionData::::get(); + let next = pallet::NextSessionChanges::::get(); - Ok(()) + assert_eq!(old_active.collator_reward, active.collator_reward); + assert_eq!(old_active.collator_count, active.collator_count); + assert_eq!(old_next.collators, next.collators); + assert_eq!(old_next.collator_count, next.collator_count); + assert_eq!(old_next.collator_reward, next.collator_reward); + assert_eq!( + next.treasury_inflation_rate, + Some(inflation_rate::(InflationPercentage::get())) + ); + assert_eq!( + Pallet::::current_storage_version(), + Pallet::::on_chain_storage_version() + ); + + log::info!("{LOG_PREFIX} POST UPGRADE: Finished"); + Ok(()) + } } } diff --git a/pallets/block-rewards/src/mock.rs b/pallets/block-rewards/src/mock.rs index c9dab65ca7..36079687fa 100644 --- a/pallets/block-rewards/src/mock.rs +++ b/pallets/block-rewards/src/mock.rs @@ -1,10 +1,13 @@ -use cfg_traits::rewards::AccountRewards; -use cfg_types::tokens::{CurrencyId, StakingCurrency::BlockRewards as BlockRewardsCurrency}; +use cfg_traits::{rewards::AccountRewards, Seconds}; +use cfg_types::{ + fixed_point::Rate, + tokens::{CurrencyId, StakingCurrency::BlockRewards as BlockRewardsCurrency}, +}; use frame_support::{ parameter_types, traits::{ fungibles::Inspect, tokens::WithdrawConsequence, ConstU16, ConstU32, ConstU64, - Currency as CurrencyT, GenesisBuild, OnFinalize, OnInitialize, OnUnbalanced, + GenesisBuild, OnFinalize, OnInitialize, }, PalletId, }; @@ -17,11 +20,10 @@ use sp_runtime::{ traits::{BlakeTwo256, ConvertInto, IdentityLookup}, }; -use crate::{self as pallet_block_rewards, Config, NegativeImbalanceOf}; +use crate::{self as pallet_block_rewards, Config}; pub(crate) const MAX_COLLATORS: u32 = 10; pub(crate) const SESSION_DURATION: BlockNumber = 5; -pub(crate) const TREASURY_ADDRESS: AccountId = u64::MAX; pub(crate) type AccountId = u64; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -42,6 +44,7 @@ frame_support::construct_runtime!( OrmlTokens: orml_tokens, Rewards: pallet_rewards::, Session: pallet_session, + MockTime: cfg_mocks::pallet_mock_time, BlockRewards: pallet_block_rewards, } ); @@ -139,18 +142,6 @@ impl pallet_balances::Config for Test { type WeightInfo = (); } -parameter_types! { - pub static RewardRemainderUnbalanced: Balance = 0; -} - -/// Mock implementation of Treasury. -pub struct RewardRemainderMock; -impl OnUnbalanced> for RewardRemainderMock { - fn on_nonzero_unbalanced(amount: NegativeImbalanceOf) { - let _ = Balances::resolve_creating(&TREASURY_ADDRESS, amount); - } -} - orml_traits::parameter_type_with_key! { pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance { match currency_id { @@ -161,7 +152,7 @@ orml_traits::parameter_type_with_key! { } impl orml_tokens::Config for Test { - type Amount = i64; + type Amount = i128; type Balance = Balance; type CurrencyHooks = (); type CurrencyId = CurrencyId; @@ -216,44 +207,44 @@ impl pallet_rewards::Config for Test { pallet_rewards::issuance::MintReward; type RewardMechanism = pallet_rewards::mechanism::base::Mechanism< Balance, - i64, + i128, sp_runtime::FixedI128, MaxCurrencyMovements, >; type RuntimeEvent = RuntimeEvent; } -// pub type MockRewards = -// cfg_traits::rewards::mock::MockRewards; +impl cfg_mocks::pallet_mock_time::Config for Test { + type Moment = Seconds; +} frame_support::parameter_types! { #[derive(scale_info::TypeInfo)] pub const MaxGroups: u32 = 1; #[derive(scale_info::TypeInfo, Debug, PartialEq, Clone)] - pub const MaxChangesPerSession: u32 = 50; - #[derive(scale_info::TypeInfo, Debug, PartialEq, Clone)] pub const MaxCollators: u32 = MAX_COLLATORS; pub const BlockRewardCurrency: CurrencyId = CurrencyId::Staking(BlockRewardsCurrency); pub const StakeAmount: Balance = cfg_types::consts::rewards::DEFAULT_COLLATOR_STAKE; pub const CollatorGroupId: u32 = cfg_types::ids::COLLATOR_GROUP_ID; + pub const TreasuryPalletId: PalletId = cfg_types::ids::TREASURY_PALLET_ID; } impl pallet_block_rewards::Config for Test { type AdminOrigin = EnsureRoot; type AuthorityId = UintAuthorityId; type Balance = Balance; - type Beneficiary = RewardRemainderMock; - type Currency = Tokens; type CurrencyId = CurrencyId; type ExistentialDeposit = ExistentialDeposit; - type MaxChangesPerSession = MaxChangesPerSession; type MaxCollators = MaxCollators; + type Rate = Rate; type Rewards = Rewards; type RuntimeEvent = RuntimeEvent; type StakeAmount = StakeAmount; type StakeCurrencyId = BlockRewardCurrency; type StakeGroupId = CollatorGroupId; + type Time = MockTime; + type Tokens = Tokens; + type TreasuryPalletId = TreasuryPalletId; type Weight = u64; type WeightInfo = (); } @@ -261,11 +252,11 @@ impl pallet_block_rewards::Config for Test { pub(crate) fn assert_staked(who: &AccountId) { assert_eq!( // NOTE: This is now the ED instead of 0, as we collators need ED now. - ::Currency::balance(::StakeCurrencyId::get(), who), + Tokens::balance(BlockRewardCurrency::get(), who), ExistentialDeposit::get() ); assert_eq!( - ::Currency::can_withdraw( + ::Tokens::can_withdraw( ::StakeCurrencyId::get(), who, ExistentialDeposit::get() * 2 @@ -281,7 +272,7 @@ pub(crate) fn assert_not_staked(who: &AccountId, was_before: bool) { ) .is_zero()); assert_eq!( - ::Currency::balance(::StakeCurrencyId::get(), who), + ::Tokens::balance(::StakeCurrencyId::get(), who), // NOTE: IF a collator has been staked before the system already granted them ED // of `StakeCurrency`. if was_before { @@ -333,7 +324,7 @@ pub(crate) fn advance_session() { pub(crate) struct ExtBuilder { collator_reward: Balance, - total_reward: Balance, + treasury_inflation_rate: Rate, run_to_block: BlockNumber, } @@ -341,7 +332,7 @@ impl Default for ExtBuilder { fn default() -> Self { Self { collator_reward: Balance::zero(), - total_reward: Balance::zero(), + treasury_inflation_rate: Rate::zero(), run_to_block: BlockNumber::one(), } } @@ -353,8 +344,8 @@ impl ExtBuilder { self } - pub(crate) fn set_total_reward(mut self, reward: Balance) -> Self { - self.total_reward = reward; + pub(crate) fn set_treasury_inflation_rate(mut self, rate: Rate) -> Self { + self.treasury_inflation_rate = rate; self } @@ -371,7 +362,8 @@ impl ExtBuilder { pallet_block_rewards::GenesisConfig:: { collators: vec![1], collator_reward: self.collator_reward, - total_reward: self.total_reward, + treasury_inflation_rate: self.treasury_inflation_rate, + last_update: 0, } .assimilate_storage(&mut storage) .expect("BlockRewards pallet's storage can be assimilated"); @@ -399,6 +391,7 @@ impl ExtBuilder { let mut ext = sp_io::TestExternalities::new(storage); ext.execute_with(|| { + MockTime::mock_now(|| 0); run_to_block(self.run_to_block); }); diff --git a/pallets/block-rewards/src/tests.rs b/pallets/block-rewards/src/tests.rs index fa0ea731de..ad7e8df2c3 100644 --- a/pallets/block-rewards/src/tests.rs +++ b/pallets/block-rewards/src/tests.rs @@ -1,5 +1,10 @@ -use cfg_types::tokens::{CurrencyId, StakingCurrency}; +use cfg_primitives::{CFG, SECONDS_PER_YEAR}; +use cfg_types::{ + fixed_point::Rate, + tokens::{CurrencyId, StakingCurrency}, +}; use frame_support::{assert_noop, assert_ok, traits::fungibles}; +use num_traits::One; use sp_runtime::traits::BadOrigin; use super::*; @@ -8,17 +13,17 @@ use crate::mock::*; // The Reward amount // NOTE: This value needs to be > ExistentialDeposit, otherwise the tests will // fail as it's not allowed to transfer a value below the ED threshold. -const REWARD: u128 = 100 + ExistentialDeposit::get(); +const REWARD: u128 = 100 * CFG + ExistentialDeposit::get(); #[test] fn check_special_privileges() { ExtBuilder::default().build().execute_with(|| { assert_noop!( - BlockRewards::set_collator_reward(RuntimeOrigin::signed(2), 10), + BlockRewards::set_collator_reward_per_session(RuntimeOrigin::signed(2), 10), BadOrigin ); assert_noop!( - BlockRewards::set_total_reward(RuntimeOrigin::signed(2), 100), + BlockRewards::set_annual_treasury_inflation_rate(RuntimeOrigin::signed(2), Rate::one()), BadOrigin ); }); @@ -26,138 +31,68 @@ fn check_special_privileges() { #[test] fn collator_reward_change() { - ExtBuilder::default() - .set_total_reward(REWARD) - .build() - .execute_with(|| { - // EPOCH 0 - assert_ok!(BlockRewards::set_collator_reward( - RuntimeOrigin::root(), - REWARD - )); - assert_eq!( - NextSessionChanges::::get().collator_reward, - Some(REWARD) - ); - assert_eq!(ActiveSessionData::::get().collator_reward, 0); - - advance_session(); - - // EPOCH 1 - assert_eq!(NextSessionChanges::::get().collator_reward, None); - assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); - - advance_session(); - - // EPOCH 2 - assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); - }); -} - -#[test] -fn collator_reward_change_throws() { - ExtBuilder::default() - .set_total_reward(1) - .set_collator_reward(0) - .build() - .execute_with(|| { - assert_ok!(BlockRewards::set_collator_reward(RuntimeOrigin::root(), 1)); - assert_noop!( - BlockRewards::set_collator_reward(RuntimeOrigin::root(), 2), - Error::::InsufficientTotalReward - ); - }); -} - -#[test] -fn total_reward_change_isolated() { ExtBuilder::default().build().execute_with(|| { // EPOCH 0 - assert_ok!(BlockRewards::set_total_reward( + assert_ok!(BlockRewards::set_collator_reward_per_session( RuntimeOrigin::root(), REWARD )); - assert_eq!(NextSessionChanges::::get().total_reward, Some(REWARD)); - assert_eq!(ActiveSessionData::::get().total_reward, 0); + assert_eq!( + NextSessionChanges::::get().collator_reward, + Some(REWARD) + ); + assert_eq!(ActiveSessionData::::get().collator_reward, 0); advance_session(); // EPOCH 1 - assert_eq!(NextSessionChanges::::get().total_reward, None); - assert_eq!(ActiveSessionData::::get().total_reward, REWARD); + assert_eq!(NextSessionChanges::::get().collator_reward, None); + assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); advance_session(); // EPOCH 2 - assert_eq!(ActiveSessionData::::get().total_reward, REWARD); + assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); }); } #[test] -fn total_reward_change_over_sessions() { - ExtBuilder::default() - .set_total_reward(REWARD) - .build() - .execute_with(|| { - // EPOCH 0 - assert_ok!(BlockRewards::set_collator_reward( - RuntimeOrigin::root(), - REWARD - )); - assert_ok!(BlockRewards::set_total_reward( - RuntimeOrigin::root(), - REWARD - )); - assert_eq!( - NextSessionChanges::::get().collator_reward, - Some(REWARD) - ); - assert_eq!(ActiveSessionData::::get().collator_reward, 0); - assert_eq!(NextSessionChanges::::get().total_reward, Some(REWARD)); - assert_eq!(ActiveSessionData::::get().total_reward, REWARD); +fn total_reward_change_isolated() { + ExtBuilder::default().build().execute_with(|| { + // EPOCH 0 + assert_ok!(BlockRewards::set_annual_treasury_inflation_rate( + RuntimeOrigin::root(), + Rate::one() + )); + assert_eq!( + NextSessionChanges::::get().treasury_inflation_rate, + Some(Rate::one()) + ); + assert_eq!( + ActiveSessionData::::get().treasury_inflation_rate, + Rate::zero() + ); - advance_session(); + advance_session(); - // EPOCH 1 - assert_eq!(NextSessionChanges::::get().collator_reward, None); - assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); - assert_eq!(NextSessionChanges::::get().total_reward, None); - assert_eq!(ActiveSessionData::::get().total_reward, REWARD); - - // Total reward update must be at least 2 * collator_reward since collator size - // increases by one - assert_eq!(ActiveSessionData::::get().collator_count, 1); - assert_eq!(NextSessionChanges::::get().collator_count, Some(2)); - assert_noop!( - BlockRewards::set_total_reward(RuntimeOrigin::root(), 2 * REWARD - 1), - Error::::InsufficientTotalReward - ); - assert_ok!(BlockRewards::set_total_reward( - RuntimeOrigin::root(), - 2 * REWARD - )); - assert_eq!( - NextSessionChanges::::get().total_reward, - Some(2 * REWARD) - ); + // EPOCH 1 + assert_eq!( + NextSessionChanges::::get().treasury_inflation_rate, + None + ); + assert_eq!( + ActiveSessionData::::get().treasury_inflation_rate, + Rate::one() + ); - advance_session(); + advance_session(); - // EPOCH 2 - assert_eq!(NextSessionChanges::::get().collator_reward, None); - assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); - assert_eq!(NextSessionChanges::::get().total_reward, None); - assert_eq!(ActiveSessionData::::get().total_reward, 2 * REWARD); - - // Total reward update must be at least 3 * collator_reward since collator size - // increases by one - assert_eq!(ActiveSessionData::::get().collator_count, 2); - assert_eq!(NextSessionChanges::::get().collator_count, Some(3)); - assert_noop!( - BlockRewards::set_total_reward(RuntimeOrigin::root(), 3 * REWARD - 1), - Error::::InsufficientTotalReward - ); - }); + // EPOCH 2 + assert_eq!( + ActiveSessionData::::get().treasury_inflation_rate, + Rate::one() + ); + }); } #[test] @@ -247,7 +182,6 @@ fn joining_leaving_collators() { fn single_claim_reward() { ExtBuilder::default() .set_collator_reward(REWARD) - .set_total_reward(10 * REWARD) .build() .execute_with(|| { assert!(::Rewards::is_ready( @@ -258,8 +192,6 @@ fn single_claim_reward() { ::StakeAmount::get() as u128 ); assert_eq!(ActiveSessionData::::get().collator_reward, REWARD); - assert_eq!(ActiveSessionData::::get().total_reward, 10 * REWARD); - assert_eq!(mock::RewardRemainderUnbalanced::get(), 0); // EPOCH 0 -> EPOCH 1 advance_session(); @@ -278,7 +210,7 @@ fn single_claim_reward() { ); assert_ok!(BlockRewards::claim_reward(RuntimeOrigin::signed(2), 1)); - System::assert_last_event(mock::RuntimeEvent::Rewards( + System::assert_last_event(RuntimeEvent::Rewards( pallet_rewards::Event::RewardClaimed { group_id: ::StakeGroupId::get(), currency_id: ::StakeCurrencyId::get(), @@ -286,10 +218,14 @@ fn single_claim_reward() { amount: REWARD, }, )); - assert_eq!(Balances::total_balance(&TREASURY_ADDRESS), 9 * REWARD); + // NOTE: Was not set + assert_eq!( + Balances::total_balance(&TreasuryPalletId::get().into_account_truncating()), + 0 + ); assert_eq!( Balances::total_issuance(), - 10 * REWARD + ExistentialDeposit::get() + REWARD + ExistentialDeposit::get() ); assert_eq!(Balances::free_balance(&1), REWARD); }); @@ -297,16 +233,24 @@ fn single_claim_reward() { #[test] fn collator_rewards_greater_than_remainder() { + let rate = Rate::saturating_from_rational(1, 10); + ExtBuilder::default() .set_collator_reward(REWARD) - .set_total_reward(2 * REWARD) + .set_treasury_inflation_rate(rate) .build() .execute_with(|| { - // EPOCH 0 -> EPOCH 1 + let initial_treasury_balance = + Balances::free_balance(&TreasuryPalletId::get().into_account_truncating()); + + // EPOCH 0 -> EPOCH + let total_issuance = ExistentialDeposit::get(); + assert_eq!(Balances::total_issuance(), total_issuance); + MockTime::mock_now(|| SECONDS_PER_YEAR * 1000); advance_session(); // EPOCH 0 had one collator [1]. - // Thus, equal distribution of total_reward to collator and Treasury. + // Thus, equal they get all. assert_eq!( ::Rewards::compute_reward( ::StakeCurrencyId::get(), @@ -314,17 +258,26 @@ fn collator_rewards_greater_than_remainder() { ), Ok(REWARD) ); + let total_issuance_without_treasury = total_issuance + REWARD; + let treasury_inflation = total_issuance_without_treasury / 10; assert_eq!( Balances::total_issuance(), - 2 * REWARD + ExistentialDeposit::get() + total_issuance_without_treasury + treasury_inflation + ); + assert_eq!( + Balances::free_balance(&TreasuryPalletId::get().into_account_truncating()), + initial_treasury_balance + treasury_inflation ); - assert_eq!(Balances::total_balance(&TREASURY_ADDRESS), REWARD); // EPOCH 1 -> EPOCH 2 + MockTime::mock_now(|| 2 * SECONDS_PER_YEAR * 1000); advance_session(); // EPOCH 1 had one collator [1]. - // Thus, equal distribution of total_reward to collator and Treasury. + // Thus, reward is minted only once. + let total_issuance_without_treasury = total_issuance_without_treasury + REWARD; + let treasury_inflation = + treasury_inflation + (treasury_inflation + total_issuance_without_treasury) / 10; assert_eq!( ::Rewards::compute_reward( ::StakeCurrencyId::get(), @@ -332,17 +285,21 @@ fn collator_rewards_greater_than_remainder() { ), Ok(2 * REWARD) ); - assert_eq!(Balances::total_balance(&TREASURY_ADDRESS), 2 * REWARD); assert_eq!( Balances::total_issuance(), - 4 * REWARD + ExistentialDeposit::get() + total_issuance_without_treasury + treasury_inflation + ); + assert_eq!( + Balances::free_balance(&TreasuryPalletId::get().into_account_truncating()), + initial_treasury_balance + treasury_inflation ); // EPOCH 2 -> EPOCH 3 + MockTime::mock_now(|| 3 * SECONDS_PER_YEAR * 1000); advance_session(); // EPOCH 2 had two collators [2, 3]. - // Thus, both consume the entire total_reward. + // Thus, both receive the reward. // Additionally, 1 should not have higher claimable reward. assert_eq!( ::Rewards::compute_reward( @@ -360,43 +317,52 @@ fn collator_rewards_greater_than_remainder() { Ok(REWARD) ); } - assert_eq!(Balances::total_balance(&TREASURY_ADDRESS), 2 * REWARD); + let total_issuance_without_treasury = total_issuance_without_treasury + 2 * REWARD; + let treasury_inflation = + treasury_inflation + (treasury_inflation + total_issuance_without_treasury) / 10; assert_eq!( Balances::total_issuance(), - 6 * REWARD + ExistentialDeposit::get() + total_issuance_without_treasury + treasury_inflation + ); + assert_eq!( + Balances::free_balance(&TreasuryPalletId::get().into_account_truncating()), + initial_treasury_balance + treasury_inflation ); // EPOCH 3 -> EPOCH 4 + MockTime::mock_now(|| 4 * SECONDS_PER_YEAR * 1000); advance_session(); // EPOCH 3 had three collators [3, 4, 5]. - // Thus, all three consume the entire total_reward - // and reseive less than collator_reward each. - assert_eq!( - ::Rewards::compute_reward( - ::StakeCurrencyId::get(), - &3 - ), - Ok(REWARD + 2 * REWARD / 3) - ); - assert_eq!( - ::Rewards::compute_reward( - ::StakeCurrencyId::get(), - &4 - ), - Ok(2 * REWARD / 3) - ); + // Thus, all three get the reward whereas [1, 2] do not. + for collator in [1, 3].iter() { + assert_eq!( + ::Rewards::compute_reward( + ::StakeCurrencyId::get(), + collator + ), + Ok(2 * REWARD) + ); + } + for collator in [2, 4, 5].iter() { + assert_eq!( + ::Rewards::compute_reward( + ::StakeCurrencyId::get(), + collator + ), + Ok(REWARD) + ); + } + let total_issuance_without_treasury = total_issuance_without_treasury + 3 * REWARD; + let treasury_inflation = + treasury_inflation + (treasury_inflation + total_issuance_without_treasury) / 10; assert_eq!( - ::Rewards::compute_reward( - ::StakeCurrencyId::get(), - &5 - ), - Ok(2 * REWARD / 3) + Balances::total_issuance(), + total_issuance_without_treasury + treasury_inflation ); - assert_eq!(Balances::total_balance(&TREASURY_ADDRESS), 2 * REWARD); assert_eq!( - Balances::total_issuance(), - 8 * REWARD + ExistentialDeposit::get() + Balances::free_balance(&TreasuryPalletId::get().into_account_truncating()), + initial_treasury_balance + treasury_inflation ); }); } @@ -405,7 +371,6 @@ fn collator_rewards_greater_than_remainder() { fn late_claiming_works() { ExtBuilder::default() .set_collator_reward(REWARD) - .set_total_reward(2 * REWARD) .set_run_to_block(100) .build() .execute_with(|| { @@ -432,7 +397,6 @@ fn late_claiming_works() { fn duplicate_claiming_works_but_ineffective() { ExtBuilder::default() .set_collator_reward(REWARD) - .set_total_reward(2 * REWARD) .set_run_to_block(100) .build() .execute_with(|| { @@ -471,3 +435,20 @@ fn duplicate_claiming_works_but_ineffective() { )); }); } + +#[test] +fn calculate_epoch_treasury_inflation() { + let rate = Rate::saturating_from_rational(1, 10); + + ExtBuilder::default().build().execute_with(|| { + MockTime::mock_now(|| 0); + let inflation = Balances::total_issuance(); + assert_eq!(BlockRewards::calculate_epoch_treasury_inflation(rate, 0), 0); + + MockTime::mock_now(|| SECONDS_PER_YEAR * 1000); + assert_eq!( + BlockRewards::calculate_epoch_treasury_inflation(rate, 0), + inflation / 10 + ); + }) +} diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 030437e4df..cd01ac4a91 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -1236,20 +1236,18 @@ impl pallet_block_rewards::Config for Runtime { type AdminOrigin = EnsureRootOr; type AuthorityId = AuraId; type Balance = Balance; - // Must not set this as long as we don't want to mint the rewards. - // By setting this to (), the remainder of TotalRewards - CollatorRewards - // will be dropped. Else it would be transferred to the configured Beneficiary. - type Beneficiary = (); - type Currency = Tokens; type CurrencyId = CurrencyId; type ExistentialDeposit = ExistentialDeposit; - type MaxChangesPerSession = MaxChangesPerEpoch; type MaxCollators = MaxAuthorities; + type Rate = Rate; type Rewards = BlockRewardsBase; type RuntimeEvent = RuntimeEvent; type StakeAmount = StakeAmount; type StakeCurrencyId = BlockRewardCurrency; type StakeGroupId = CollatorGroupId; + type Time = Timestamp; + type Tokens = Tokens; + type TreasuryPalletId = TreasuryPalletId; type Weight = u64; type WeightInfo = weights::pallet_block_rewards::WeightInfo; } diff --git a/runtime/altair/src/migrations.rs b/runtime/altair/src/migrations.rs index fcd445cc08..f02f2175f5 100644 --- a/runtime/altair/src/migrations.rs +++ b/runtime/altair/src/migrations.rs @@ -23,6 +23,7 @@ use orml_traits::asset_registry::AssetMetadata; frame_support::parameter_types! { pub const NftSalesPalletName: &'static str = "NftSales"; pub const MigrationPalletName: &'static str = "Migration"; + pub const AnnualTreasuryInflationPercent: u32 = 3; } /// The migration set for Altair 1034 @ Kusama. It includes all the migrations @@ -75,6 +76,11 @@ pub type UpgradeAltair1034 = ( runtime_common::migrations::nuke::KillPallet, // Removes unused migration pallet runtime_common::migrations::nuke::KillPallet, + // Apply relative treasury inflation + pallet_block_rewards::migrations::v2::RelativeTreasuryInflationMigration< + crate::Runtime, + AnnualTreasuryInflationPercent, + >, ); #[allow(clippy::upper_case_acronyms)] diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index 161cc3790b..aac3b86a47 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -1252,18 +1252,19 @@ impl pallet_block_rewards::Config for Runtime { type AdminOrigin = EnsureRootOr; type AuthorityId = AuraId; type Balance = Balance; - // Must not change this as long as we want to mint rewards into the treasury - type Beneficiary = Treasury; - type Currency = Tokens; type CurrencyId = CurrencyId; type ExistentialDeposit = ExistentialDeposit; - type MaxChangesPerSession = MaxChangesPerEpoch; type MaxCollators = MaxAuthorities; + type Rate = Rate; type Rewards = BlockRewardsBase; type RuntimeEvent = RuntimeEvent; type StakeAmount = StakeAmount; type StakeCurrencyId = BlockRewardCurrency; type StakeGroupId = CollatorGroupId; + type Time = Timestamp; + type Tokens = Tokens; + // Must not change this as long as we want to mint rewards into the treasury + type TreasuryPalletId = TreasuryPalletId; type Weight = u64; type WeightInfo = weights::pallet_block_rewards::WeightInfo; } diff --git a/runtime/centrifuge/src/migrations.rs b/runtime/centrifuge/src/migrations.rs index 1d42f68632..29405b277f 100644 --- a/runtime/centrifuge/src/migrations.rs +++ b/runtime/centrifuge/src/migrations.rs @@ -33,6 +33,7 @@ frame_support::parameter_types! { pub const UsdcArb: CurrencyId = CURRENCY_ID_LP_ARB; pub const UsdcCelo: CurrencyId = CURRENCY_ID_LP_CELO; pub const MinOrderAmount: Balance = 10u128.pow(6); + pub const AnnualTreasuryInflationPercent: u32 = 3; } pub type UpgradeCentrifuge1025 = ( @@ -69,6 +70,11 @@ pub type UpgradeCentrifuge1025 = ( runtime_common::migrations::precompile_account_codes::Migration, // Bumps storage version from 0 to 1 runtime_common::migrations::nuke::ResetPallet, + // Apply relative treasury inflation + pallet_block_rewards::migrations::v2::RelativeTreasuryInflationMigration< + crate::Runtime, + AnnualTreasuryInflationPercent, + >, ); // Copyright 2021 Centrifuge Foundation (centrifuge.io). diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 815aa46192..cbc800d969 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -1745,17 +1745,18 @@ impl pallet_block_rewards::Config for Runtime { type AdminOrigin = EnsureRootOr; type AuthorityId = AuraId; type Balance = Balance; - type Beneficiary = Treasury; - type Currency = Tokens; type CurrencyId = CurrencyId; type ExistentialDeposit = ExistentialDeposit; - type MaxChangesPerSession = MaxChangesPerEpoch; type MaxCollators = MaxAuthorities; + type Rate = Rate; type Rewards = BlockRewardsBase; type RuntimeEvent = RuntimeEvent; type StakeAmount = StakeAmount; type StakeCurrencyId = BlockRewardCurrency; type StakeGroupId = CollatorGroupId; + type Time = Timestamp; + type Tokens = Tokens; + type TreasuryPalletId = TreasuryPalletId; type Weight = u64; type WeightInfo = weights::pallet_block_rewards::WeightInfo; } diff --git a/runtime/development/src/migrations.rs b/runtime/development/src/migrations.rs index 0917402f5d..52b98c8e00 100644 --- a/runtime/development/src/migrations.rs +++ b/runtime/development/src/migrations.rs @@ -20,6 +20,7 @@ frame_support::parameter_types! { pub const LocalAssetIdUsdc: LocalAssetId = LOCAL_ASSET_ID; pub const LocalCurrencyIdUsdc: CurrencyId = CURRENCY_ID_LOCAL; pub const PoolCurrencyAnemoy: CurrencyId = CURRENCY_ID_DOT_NATIVE; + pub const AnnualTreasuryInflationPercent: u32 = 3; } pub type UpgradeDevelopment1041 = ( @@ -43,4 +44,9 @@ pub type UpgradeDevelopment1041 = ( crate::RocksDbWeight, 0, >, + // Apply relative treasury inflation + pallet_block_rewards::migrations::v2::RelativeTreasuryInflationMigration< + crate::Runtime, + AnnualTreasuryInflationPercent, + >, ); diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index 34fae9b269..71a8e586af 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -4031,7 +4031,6 @@ mod development { let sender = ::Sender::get(); - dbg!(frame_system::Pallet::::events()); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { @@ -4133,7 +4132,6 @@ mod development { increase_msg )); - dbg!(frame_system::Pallet::::events()); assert!(frame_system::Pallet::::events().iter().any(|e| { e.event == pallet_liquidity_pools_gateway::Event::::OutboundMessageSubmitted { diff --git a/runtime/integration-tests/src/utils/genesis.rs b/runtime/integration-tests/src/utils/genesis.rs index 0462b69930..a2dc3d70fb 100644 --- a/runtime/integration-tests/src/utils/genesis.rs +++ b/runtime/integration-tests/src/utils/genesis.rs @@ -11,10 +11,13 @@ // GNU General Public License for more details. //! Utilitites around populating a genesis storage -use cfg_types::tokens::{CurrencyId, CustomMetadata}; +use cfg_types::{ + fixed_point::Rate, + tokens::{CurrencyId, CustomMetadata}, +}; use frame_support::traits::GenesisBuild; use serde::{Deserialize, Serialize}; -use sp_runtime::{AccountId32, Storage}; +use sp_runtime::{AccountId32, FixedPointNumber, Storage}; use crate::utils::{ accounts::{default_accounts, Keyring}, @@ -215,11 +218,13 @@ where Runtime::AccountId: From, Runtime: pallet_block_rewards::Config, ::Balance: From, + ::Rate: From, { pallet_block_rewards::GenesisConfig:: { collators: vec![Keyring::Admin.to_account_id().into()], collator_reward: (1000 * cfg_primitives::CFG).into(), - total_reward: (10_000 * cfg_primitives::CFG).into(), + treasury_inflation_rate: Rate::saturating_from_rational(3, 100).into(), + last_update: Default::default(), } .assimilate_storage(storage) .expect("ESSENTIAL: Genesisbuild is not allowed to fail.");