diff --git a/node/src/parachain/dev_chain_spec.rs b/node/src/parachain/dev_chain_spec.rs index 0f0f920e..c9b07740 100644 --- a/node/src/parachain/dev_chain_spec.rs +++ b/node/src/parachain/dev_chain_spec.rs @@ -12,7 +12,7 @@ use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{sr25519, Pair, Public}; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Perbill, + Perbill, Permill, }; /// Specialized `ChainSpec`. This is a specialization of the general Substrate ChainSpec type. @@ -133,6 +133,8 @@ fn configure_genesis( parachain_staking: ParachainStakingConfig { stakers, max_candidate_stake: staking::MAX_COLLATOR_STAKE, + max_commission_change: Permill::from_percent(100), + min_commission_change_interval: 0, }, inflation_manager: Default::default(), block_reward: BlockRewardConfig { diff --git a/node/src/parachain/krest_chain_spec.rs b/node/src/parachain/krest_chain_spec.rs index 347386ae..a361a7d4 100644 --- a/node/src/parachain/krest_chain_spec.rs +++ b/node/src/parachain/krest_chain_spec.rs @@ -9,7 +9,7 @@ use peaq_primitives_xcm::{AccountId, Balance}; use runtime_common::TOKEN_DECIMALS; use sc_service::{ChainType, Properties}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_runtime::Perbill; +use sp_runtime::{Perbill, Permill}; use crate::parachain::dev_chain_spec::{authority_keys_from_seed, get_account_id_from_seed}; @@ -118,6 +118,8 @@ fn configure_genesis( parachain_staking: ParachainStakingConfig { stakers, max_candidate_stake: staking::MAX_COLLATOR_STAKE, + max_commission_change: Permill::from_percent(100), + min_commission_change_interval: 0, }, inflation_manager: Default::default(), block_reward: BlockRewardConfig { diff --git a/node/src/parachain/peaq_chain_spec.rs b/node/src/parachain/peaq_chain_spec.rs index c74e3f23..728043ec 100644 --- a/node/src/parachain/peaq_chain_spec.rs +++ b/node/src/parachain/peaq_chain_spec.rs @@ -9,7 +9,7 @@ use peaq_runtime::{ use runtime_common::TOKEN_DECIMALS; use sc_service::{ChainType, Properties}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_runtime::Perbill; +use sp_runtime::{Perbill, Permill}; use crate::parachain::dev_chain_spec::{authority_keys_from_seed, get_account_id_from_seed}; @@ -122,6 +122,8 @@ fn configure_genesis( parachain_staking: ParachainStakingConfig { stakers, max_candidate_stake: staking::MAX_COLLATOR_STAKE, + max_commission_change: Permill::from_percent(100), + min_commission_change_interval: 0, }, inflation_manager: Default::default(), block_reward: BlockRewardConfig { diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index 1a70f6da..0dc780fa 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -432,6 +432,10 @@ pub mod pallet { CommissionTooHigh, /// Sudo cannot force new round if payouts are ongoing PayoutsOngoing, + /// The commission change is too high. + CommissionChangeTooHigh, + /// The commission change is too frequent. + CommissionChangeTooEarly, } #[pallet::event] @@ -519,6 +523,12 @@ pub mod pallet { /// The commission for a collator has been changed. /// \[collator's account, new commission\] CollatorCommissionChanged(T::AccountId, Permill), + /// The commission maximum change has been changed. + /// \[new value\] + MaxCommissionChangeUpdated(Permill), + /// The delay between two commission changes has been changed. + /// \[new value\] + CommissionChangeIntervalUpdated(BlockNumberFor), } #[pallet::hooks] @@ -677,15 +687,36 @@ pub mod pallet { pub(crate) type DelayedPayoutInfo = StorageValue<_, DelayedPayoutInfoT>, OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn last_commission_change)] + pub type LastCommissionChange = + StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn max_commission_change)] + pub type MaxCommissionChange = StorageValue<_, Permill, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn min_commission_change_interval)] + pub type MinCommissionChangeInterval = + StorageValue<_, BlockNumberFor, ValueQuery>; + #[pallet::genesis_config] pub struct GenesisConfig { pub stakers: GenesisStaker, pub max_candidate_stake: BalanceOf, + pub max_commission_change: Permill, + pub min_commission_change_interval: BlockNumberFor, } impl Default for GenesisConfig { fn default() -> Self { - Self { stakers: Default::default(), max_candidate_stake: Default::default() } + Self { + stakers: Default::default(), + max_candidate_stake: Default::default(), + max_commission_change: Permill::from_percent(100), + min_commission_change_interval: 0u32.into(), + } } } @@ -722,6 +753,9 @@ pub mod pallet { let round: RoundInfo> = RoundInfo::new(0u32, 0u32.into(), T::DefaultBlocksPerRound::get()); >::put(round); + + MaxCommissionChange::::put(self.max_commission_change); + MinCommissionChangeInterval::::put(self.min_commission_change_interval); } } @@ -1969,20 +2003,67 @@ pub mod pallet { ))] pub fn set_commission(origin: OriginFor, commission: Permill) -> DispatchResult { let collator = ensure_signed(origin)?; - CandidatePool::::get(&collator).ok_or(Error::::CandidateNotFound)?; - if commission > Permill::from_percent(100) { - return Err(Error::::CommissionTooHigh.into()) - } + let current_block = >::block_number(); - >::mutate(&collator, |maybe_candidate| { - if let Some(candidate) = maybe_candidate { - candidate.set_commission(commission); - } - }); + // Check if the collator exists + let mut candidate = + CandidatePool::::get(&collator).ok_or(Error::::CandidateNotFound)?; + + // Check the time since the last commission change + let last_change = LastCommissionChange::::get(&collator); + ensure!( + current_block >= last_change + MinCommissionChangeInterval::::get(), + Error::::CommissionChangeTooEarly + ); + + // Check the maximum change commission + let max_change = MaxCommissionChange::::get(); + let current_commission = candidate.commission; + let change = if commission > current_commission { + commission - current_commission + } else { + current_commission - commission + }; + ensure!(change <= max_change, Error::::CommissionChangeTooHigh); + + // Update the commission and the last change time + candidate.set_commission(commission); + CandidatePool::::insert(&collator, candidate); + LastCommissionChange::::insert(&collator, current_block); + + // Emit an event that the commission was updated + Self::deposit_event(Event::CollatorCommissionChanged(collator, commission)); + Ok(()) + } + + #[pallet::call_index(20)] + #[pallet::weight(::WeightInfo::set_max_commission_change( + Permill::from_percent(100).deconstruct() + ))] + pub fn set_max_commission_change( + origin: OriginFor, + new_max_commission_change: Permill, + ) -> DispatchResult { + ensure_root(origin)?; + + MaxCommissionChange::::put(new_max_commission_change); + + Self::deposit_event(Event::MaxCommissionChangeUpdated(new_max_commission_change)); + Ok(()) + } + + #[pallet::call_index(21)] + #[pallet::weight(::WeightInfo::set_min_commission_change_interval())] + pub fn set_min_commission_change_interval( + origin: OriginFor, + new_min_commission_change_interval: BlockNumberFor, + ) -> DispatchResult { + ensure_root(origin)?; + + MinCommissionChangeInterval::::put(new_min_commission_change_interval); - // Emit an event that the commission was updated. - Self::deposit_event(crate::pallet::Event::CollatorCommissionChanged( - collator, commission, + Self::deposit_event(Event::CommissionChangeIntervalUpdated( + new_min_commission_change_interval, )); Ok(()) } diff --git a/pallets/parachain-staking/src/migrations.rs b/pallets/parachain-staking/src/migrations.rs index 6c7a9b55..d4896dd1 100644 --- a/pallets/parachain-staking/src/migrations.rs +++ b/pallets/parachain-staking/src/migrations.rs @@ -3,13 +3,14 @@ use crate::{ pallet::{Config, Pallet, OLD_STAKING_ID, STAKING_ID}, types::{Candidate, OldCandidate}, - CandidatePool, ForceNewRound, Round, + CandidatePool, ForceNewRound, MaxCommissionChange, MinCommissionChangeInterval, Round, }; use frame_support::{ pallet_prelude::{GetStorageVersion, StorageVersion}, traits::{Get, LockableCurrency, WithdrawReasons}, weights::Weight, }; +use frame_system::pallet_prelude::BlockNumberFor; use pallet_balances::Locks; use sp_runtime::Permill; @@ -20,8 +21,9 @@ pub enum Versions { _V8 = 8, V9 = 9, V10 = 10, - #[default] V11 = 11, + #[default] + V12 = 12, } pub(crate) fn on_runtime_upgrade() -> Weight { @@ -102,6 +104,12 @@ mod upgrade { log::info!("V11 Migrating Done."); } + + if onchain_storage_version < StorageVersion::new(Versions::V12 as u16) { + // Set the value of MaxCommissionChange to 10% + MaxCommissionChange::::put(Permill::from_percent(10)); + MinCommissionChangeInterval::::put(BlockNumberFor::::from(0u32)); + } // update onchain storage version StorageVersion::new(Versions::default() as u16).put::>(); weight_writes += 1; diff --git a/pallets/parachain-staking/src/mock.rs b/pallets/parachain-staking/src/mock.rs index b1816d15..a87f3a49 100644 --- a/pallets/parachain-staking/src/mock.rs +++ b/pallets/parachain-staking/src/mock.rs @@ -32,7 +32,7 @@ use sp_runtime::{ impl_opaque_keys, testing::UintAuthorityId, traits::{BlakeTwo256, ConvertInto, IdentityLookup, OpaqueKeys}, - BuildStorage, Perbill, + BuildStorage, Perbill, Permill, }; use sp_std::fmt::Debug; @@ -154,6 +154,7 @@ parameter_types! { pub const MinDelegatorStake: Balance = 5; pub const MinDelegation: Balance = 3; pub const MaxUnstakeRequests: u32 = 6; + } impl Config for Test { @@ -278,9 +279,14 @@ impl ExtBuilder { for delegator in self.delegators.clone() { stakers.push((delegator.0, Some(delegator.1), delegator.2)); } - stake::GenesisConfig:: { stakers, max_candidate_stake: 160_000_000 * DECIMALS } - .assimilate_storage(&mut t) - .expect("Parachain Staking's storage can be assimilated"); + stake::GenesisConfig:: { + stakers, + max_candidate_stake: 160_000_000 * DECIMALS, + max_commission_change: Permill::from_percent(10), + min_commission_change_interval: 1, + } + .assimilate_storage(&mut t) + .expect("Parachain Staking's storage can be assimilated"); // stashes are the AccountId let session_keys: Vec<_> = self diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index 0b3dff9f..9857b3a2 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -3990,3 +3990,143 @@ fn check_snapshot_is_cleared() { assert_eq!(at_stake.len(), 0); }); } + +#[test] +fn change_commission_change_interval() { + ExtBuilder::default() + .with_balances(vec![(1, 1000), (2, 1000), (3, 1000)]) + .with_collators(vec![(1, 500)]) + .with_delegators(vec![(2, 1, 600), (3, 1, 400)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + + assert_ok!(Balances::force_set_balance( + RawOrigin::Root.into(), + StakePallet::account_id(), + 1000, + )); + + assert_ok!(StakePallet::set_min_commission_change_interval(RuntimeOrigin::root(), 10)); + assert_eq!(StakePallet::min_commission_change_interval(), 10); + }); +} + +#[test] +fn change_commission_too_frequently() { + ExtBuilder::default() + .with_balances(vec![(1, 1000), (2, 1000), (3, 1000)]) + .with_collators(vec![(1, 500)]) + .with_delegators(vec![(2, 1, 600), (3, 1, 400)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + + assert_ok!(Balances::force_set_balance( + RawOrigin::Root.into(), + StakePallet::account_id(), + 1000, + )); + + assert_ok!(StakePallet::set_commission( + RuntimeOrigin::signed(1), + Permill::from_percent(10) + )); + let state = CandidatePool::::get(1).unwrap(); + assert_eq!(state.commission, Permill::from_percent(10)); + assert_eq!( + StakePallet::candidate_pool(1).unwrap().commission, + Permill::from_percent(10) + ); + + // change commission too frequently + assert_noop!( + StakePallet::set_commission(RuntimeOrigin::signed(1), Permill::from_percent(20)), + Error::::CommissionChangeTooEarly + ); + // change commission too frequently + assert_noop!( + StakePallet::set_commission(RuntimeOrigin::signed(1), Permill::from_percent(30)), + Error::::CommissionChangeTooEarly + ); + }); +} + +#[test] +fn change_commission_after_while() { + ExtBuilder::default() + .with_balances(vec![(1, 1000), (2, 1000), (3, 1000)]) + .with_collators(vec![(1, 500)]) + .with_delegators(vec![(2, 1, 600), (3, 1, 400)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + + assert_ok!(Balances::force_set_balance( + RawOrigin::Root.into(), + StakePallet::account_id(), + 1000, + )); + + assert_ok!(StakePallet::set_commission( + RuntimeOrigin::signed(1), + Permill::from_percent(10) + )); + let state = CandidatePool::::get(1).unwrap(); + assert_eq!(state.commission, Permill::from_percent(10)); + assert_eq!( + StakePallet::candidate_pool(1).unwrap().commission, + Permill::from_percent(10) + ); + + // change commission after a while + roll_to(10, vec![]); + assert_ok!(StakePallet::set_commission( + RuntimeOrigin::signed(1), + Permill::from_percent(20) + )); + let state = CandidatePool::::get(1).unwrap(); + assert_eq!(state.commission, Permill::from_percent(20)); + assert_eq!( + StakePallet::candidate_pool(1).unwrap().commission, + Permill::from_percent(20) + ); + }); +} + +#[test] +fn change_commission_by_too_much() { + ExtBuilder::default() + .with_balances(vec![(1, 1000), (2, 1000), (3, 1000)]) + .with_collators(vec![(1, 500)]) + .with_delegators(vec![(2, 1, 600), (3, 1, 400)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + + assert_ok!(Balances::force_set_balance( + RawOrigin::Root.into(), + StakePallet::account_id(), + 1000, + )); + + assert_ok!(StakePallet::set_commission( + RuntimeOrigin::signed(1), + Permill::from_percent(10) + )); + let state = CandidatePool::::get(1).unwrap(); + assert_eq!(state.commission, Permill::from_percent(10)); + assert_eq!( + StakePallet::candidate_pool(1).unwrap().commission, + Permill::from_percent(10) + ); + + roll_to(10, vec![]); + + // change commission by too much + assert_noop!( + StakePallet::set_commission(RuntimeOrigin::signed(1), Permill::from_percent(30)), + Error::::CommissionChangeTooHigh + ); + }); +} diff --git a/pallets/parachain-staking/src/weightinfo.rs b/pallets/parachain-staking/src/weightinfo.rs index 4c8420d1..afc21532 100644 --- a/pallets/parachain-staking/src/weightinfo.rs +++ b/pallets/parachain-staking/src/weightinfo.rs @@ -24,4 +24,6 @@ pub trait WeightInfo { fn unlock_unstaked(u: u32) -> Weight; fn set_max_candidate_stake() -> Weight; fn set_commission(n: u32, m: u32) -> Weight; + fn set_max_commission_change(n: u32) -> Weight; + fn set_min_commission_change_interval() -> Weight; } diff --git a/pallets/parachain-staking/src/weights.rs b/pallets/parachain-staking/src/weights.rs index 89557e0b..5e6113b9 100644 --- a/pallets/parachain-staking/src/weights.rs +++ b/pallets/parachain-staking/src/weights.rs @@ -537,4 +537,31 @@ impl crate::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + + /// Storage: `ParachainStaking::MaxCommissionChange` (r:1 w:1) + /// Proof: `ParachainStaking::MaxCommissionChange` (`max_values`: None, `max_size`: Some(1314), added: 3789, mode: `MaxEncodedLen`) + /// The range of component `m` is `[0, 1000000]`. + fn set_max_commission_change(_m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `393` + // Estimated: `4779` + // Minimum execution time: 19_960_000 picoseconds. + Weight::from_parts(20_647_875, 0) + .saturating_add(Weight::from_parts(0, 4779)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::MaxCommissionChangeInterval` (r:1 w:1) + /// Proof: `ParachainStaking::MaxCommissionChangeInterval` (`max_values`: None, `max_size`: Some(1314), added: 3789, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000000]`. + fn set_min_commission_change_interval() -> Weight { + // Proof Size summary in bytes: + // Measured: `393` + // Estimated: `4779` + // Minimum execution time: 19_960_000 picoseconds. + Weight::from_parts(20_647_875, 0) + .saturating_add(Weight::from_parts(0, 4779)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/precompiles/parachain-staking/src/mock.rs b/precompiles/parachain-staking/src/mock.rs index 83647784..f63f267b 100644 --- a/precompiles/parachain-staking/src/mock.rs +++ b/precompiles/parachain-staking/src/mock.rs @@ -34,7 +34,7 @@ use sp_runtime::{ impl_opaque_keys, testing::UintAuthorityId, traits::{BlakeTwo256, ConvertInto, IdentityLookup, OpaqueKeys}, - BuildStorage, Perbill, + BuildStorage, Perbill, Permill, }; use sp_std::fmt::Debug; @@ -333,6 +333,8 @@ impl ExtBuilder { parachain_staking::GenesisConfig:: { stakers, max_candidate_stake: 160_000_000 * DECIMALS, + max_commission_change: Permill::from_percent(100), + min_commission_change_interval: 1, } .assimilate_storage(&mut t) .expect("Parachain Staking's storage can be assimilated");