Skip to content

Commit

Permalink
Implements a % cap on staking rewards from era inflation (paritytech…
Browse files Browse the repository at this point in the history
…#1660)

This PR implements an (optional) cap of the era inflation that is
allocated to staking rewards. The remaining is minted directly into the
[`RewardRemainder`](https://github.com/paritytech/polkadot-sdk/blob/fb0fd3e62445eb2dee2b2456a0c8574d1ecdcc73/substrate/frame/staking/src/pallet/mod.rs#L160)
account, which is the treasury pot account in Polkadot and Kusama.

The staking pallet now has a percent storage item, `MaxStakersRewards`,
which defines the max percentage of the era inflation that should be
allocated to staking rewards. The remaining era inflation (i.e.
`remaining = max_era_payout - staking_payout.min(staking_payout *
MaxStakersRewards))` is minted directly into the treasury.

The `MaxStakersRewards` can be set by a privileged origin through the
`set_staking_configs` extrinsic.

**To finish**
- [x] run benchmarks for westend-runtime

Replaces paritytech#1483
Closes paritytech#403

---------

Co-authored-by: command-bot <>
  • Loading branch information
gpestana authored Feb 15, 2024
1 parent 5fc7622 commit fde4447
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 300 deletions.
206 changes: 105 additions & 101 deletions polkadot/runtime/westend/src/weights/pallet_staking.rs

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions prdoc/pr_1660.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
title: Implements a percentage cap on staking rewards from era inflation

doc:
- audience: Runtime Dev
description: |
The `pallet-staking` exposes a new perbill configuration, `MaxStakersRewards`, which caps the
amount of era inflation that is distributed to the stakers. The remainder of the era
inflation is minted directly into `T::RewardRemainder` account. This allows the runtime to be
configured to assign a minimum inflation value per era to a specific account (e.g. treasury).

crates:
- name: pallet-staking
1 change: 1 addition & 0 deletions substrate/frame/nomination-pools/test-staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
));
});

Expand Down
7 changes: 6 additions & 1 deletion substrate/frame/staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,14 +855,16 @@ benchmarks! {
ConfigOp::Set(u32::MAX),
ConfigOp::Set(u32::MAX),
ConfigOp::Set(Percent::max_value()),
ConfigOp::Set(Perbill::max_value())
ConfigOp::Set(Perbill::max_value()),
ConfigOp::Set(Percent::max_value())
) verify {
assert_eq!(MinNominatorBond::<T>::get(), BalanceOf::<T>::max_value());
assert_eq!(MinValidatorBond::<T>::get(), BalanceOf::<T>::max_value());
assert_eq!(MaxNominatorsCount::<T>::get(), Some(u32::MAX));
assert_eq!(MaxValidatorsCount::<T>::get(), Some(u32::MAX));
assert_eq!(ChillThreshold::<T>::get(), Some(Percent::from_percent(100)));
assert_eq!(MinCommission::<T>::get(), Perbill::from_percent(100));
assert_eq!(MaxStakedRewards::<T>::get(), Some(Percent::from_percent(100)));
}

set_staking_configs_all_remove {
Expand All @@ -873,6 +875,7 @@ benchmarks! {
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove
) verify {
assert!(!MinNominatorBond::<T>::exists());
Expand All @@ -881,6 +884,7 @@ benchmarks! {
assert!(!MaxValidatorsCount::<T>::exists());
assert!(!ChillThreshold::<T>::exists());
assert!(!MinCommission::<T>::exists());
assert!(!MaxStakedRewards::<T>::exists());
}

chill_other {
Expand All @@ -904,6 +908,7 @@ benchmarks! {
ConfigOp::Set(0),
ConfigOp::Set(Percent::from_percent(0)),
ConfigOp::Set(Zero::zero()),
ConfigOp::Noop,
)?;

let caller = whitelisted_caller();
Expand Down
14 changes: 11 additions & 3 deletions substrate/frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@
//! ```nocompile
//! remaining_payout = max_yearly_inflation * total_tokens / era_per_year - staker_payout
//! ```
//!
//! Note, however, that it is possible to set a cap on the total `staker_payout` for the era through
//! the `MaxStakersRewards` storage type. The `era_payout` implementor must ensure that the
//! `max_payout = remaining_payout + (staker_payout * max_stakers_rewards)`. The excess payout that
//! is not allocated for stakers is the era remaining reward.
//!
//! The remaining reward is send to the configurable end-point [`Config::RewardRemainder`].
//!
//! ### Reward Calculation
Expand Down Expand Up @@ -897,8 +903,10 @@ impl<Balance: Default> EraPayout<Balance> for () {
/// Adaptor to turn a `PiecewiseLinear` curve definition into an `EraPayout` impl, used for
/// backwards compatibility.
pub struct ConvertCurve<T>(sp_std::marker::PhantomData<T>);
impl<Balance: AtLeast32BitUnsigned + Clone, T: Get<&'static PiecewiseLinear<'static>>>
EraPayout<Balance> for ConvertCurve<T>
impl<Balance, T> EraPayout<Balance> for ConvertCurve<T>
where
Balance: AtLeast32BitUnsigned + Clone + Copy,
T: Get<&'static PiecewiseLinear<'static>>,
{
fn era_payout(
total_staked: Balance,
Expand All @@ -912,7 +920,7 @@ impl<Balance: AtLeast32BitUnsigned + Clone, T: Get<&'static PiecewiseLinear<'sta
// Duration of era; more than u64::MAX is rewarded as u64::MAX.
era_duration_millis,
);
let rest = max_payout.saturating_sub(validator_payout.clone());
let rest = max_payout.saturating_sub(validator_payout);
(validator_payout, rest)
}
}
Expand Down
11 changes: 10 additions & 1 deletion substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
use pallet_session::historical;
use sp_runtime::{
traits::{Bounded, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero},
Perbill,
Perbill, Percent,
};
use sp_staking::{
currency_to_vote::CurrencyToVote,
Expand Down Expand Up @@ -507,9 +507,18 @@ impl<T: Config> Pallet<T> {
.saturated_into::<u64>();
let staked = Self::eras_total_stake(&active_era.index);
let issuance = T::Currency::total_issuance();

let (validator_payout, remainder) =
T::EraPayout::era_payout(staked, issuance, era_duration);

let total_payout = validator_payout.saturating_add(remainder);
let max_staked_rewards =
MaxStakedRewards::<T>::get().unwrap_or(Percent::from_percent(100));

// apply cap to validators payout and add difference to remainder.
let validator_payout = validator_payout.min(max_staked_rewards * total_payout);
let remainder = total_payout.saturating_sub(validator_payout);

Self::deposit_event(Event::<T>::EraPaid {
era_index: active_era.index,
validator_payout,
Expand Down
8 changes: 8 additions & 0 deletions substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,12 @@ pub mod pallet {
#[pallet::getter(fn force_era)]
pub type ForceEra<T> = StorageValue<_, Forcing, ValueQuery>;

/// Maximum staked rewards, i.e. the percentage of the era inflation that
/// is used for stake rewards.
/// See [Era payout](./index.html#era-payout).
#[pallet::storage]
pub type MaxStakedRewards<T> = StorageValue<_, Percent, OptionQuery>;

/// The percentage of the slash that is distributed to reporters.
///
/// The rest of the slashed value is handled by the `Slash`.
Expand Down Expand Up @@ -1717,6 +1723,7 @@ pub mod pallet {
max_validator_count: ConfigOp<u32>,
chill_threshold: ConfigOp<Percent>,
min_commission: ConfigOp<Perbill>,
max_staked_rewards: ConfigOp<Percent>,
) -> DispatchResult {
ensure_root(origin)?;

Expand All @@ -1736,6 +1743,7 @@ pub mod pallet {
config_op_exp!(MaxValidatorsCount<T>, max_validator_count);
config_op_exp!(ChillThreshold<T>, chill_threshold);
config_op_exp!(MinCommission<T>, min_commission);
config_op_exp!(MaxStakedRewards<T>, max_staked_rewards);
Ok(())
}
/// Declare a `controller` to stop participating as either a validator or nominator.
Expand Down
88 changes: 84 additions & 4 deletions substrate/frame/staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fn set_staking_configs_works() {
ConfigOp::Set(10),
ConfigOp::Set(20),
ConfigOp::Set(Percent::from_percent(75)),
ConfigOp::Set(Zero::zero()),
ConfigOp::Set(Zero::zero())
));
assert_eq!(MinNominatorBond::<Test>::get(), 1_500);
Expand All @@ -63,6 +64,7 @@ fn set_staking_configs_works() {
assert_eq!(MaxValidatorsCount::<Test>::get(), Some(20));
assert_eq!(ChillThreshold::<Test>::get(), Some(Percent::from_percent(75)));
assert_eq!(MinCommission::<Test>::get(), Perbill::from_percent(0));
assert_eq!(MaxStakedRewards::<Test>::get(), Some(Percent::from_percent(0)));

// noop does nothing
assert_storage_noop!(assert_ok!(Staking::set_staking_configs(
Expand All @@ -72,6 +74,7 @@ fn set_staking_configs_works() {
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop
)));

Expand All @@ -83,6 +86,7 @@ fn set_staking_configs_works() {
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove
));
assert_eq!(MinNominatorBond::<Test>::get(), 0);
Expand All @@ -91,6 +95,7 @@ fn set_staking_configs_works() {
assert_eq!(MaxValidatorsCount::<Test>::get(), None);
assert_eq!(ChillThreshold::<Test>::get(), None);
assert_eq!(MinCommission::<Test>::get(), Perbill::from_percent(0));
assert_eq!(MaxStakedRewards::<Test>::get(), None);
});
}

Expand Down Expand Up @@ -1739,6 +1744,74 @@ fn rebond_emits_right_value_in_event() {
});
}

#[test]
fn max_staked_rewards_default_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(<MaxStakedRewards<Test>>::get(), None);

let default_stakers_payout = current_total_payout_for_duration(reward_time_per_era());
assert!(default_stakers_payout > 0);
start_active_era(1);

// the final stakers reward is the same as the reward before applied the cap.
assert_eq!(ErasValidatorReward::<Test>::get(0).unwrap(), default_stakers_payout);

// which is the same behaviour if the `MaxStakedRewards` is set to 100%.
<MaxStakedRewards<Test>>::set(Some(Percent::from_parts(100)));

let default_stakers_payout = current_total_payout_for_duration(reward_time_per_era());
assert_eq!(ErasValidatorReward::<Test>::get(0).unwrap(), default_stakers_payout);
})
}

#[test]
fn max_staked_rewards_works() {
ExtBuilder::default().nominate(true).build_and_execute(|| {
let max_staked_rewards = 10;

// sets new max staked rewards through set_staking_configs.
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Set(Percent::from_percent(max_staked_rewards)),
));

assert_eq!(<MaxStakedRewards<Test>>::get(), Some(Percent::from_percent(10)));

// check validators account state.
assert_eq!(Session::validators().len(), 2);
assert!(Session::validators().contains(&11) & Session::validators().contains(&21));
// balance of the mock treasury account is 0
assert_eq!(RewardRemainderUnbalanced::get(), 0);

let max_stakers_payout = current_total_payout_for_duration(reward_time_per_era());

start_active_era(1);

let treasury_payout = RewardRemainderUnbalanced::get();
let validators_payout = ErasValidatorReward::<Test>::get(0).unwrap();
let total_payout = treasury_payout + validators_payout;

// max stakers payout (without max staked rewards cap applied) is larger than the final
// validator rewards. The final payment and remainder should be adjusted by redestributing
// the era inflation to apply the cap...
assert!(max_stakers_payout > validators_payout);

// .. which means that the final validator payout is 10% of the total payout..
assert_eq!(validators_payout, Percent::from_percent(max_staked_rewards) * total_payout);
// .. and the remainder 90% goes to the treasury.
assert_eq!(
treasury_payout,
Percent::from_percent(100 - max_staked_rewards) * (treasury_payout + validators_payout)
);
})
}

#[test]
fn reward_to_stake_works() {
ExtBuilder::default()
Expand Down Expand Up @@ -5543,7 +5616,8 @@ fn chill_other_works() {
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove
ConfigOp::Remove,
ConfigOp::Noop,
));

// Still can't chill these users
Expand All @@ -5564,7 +5638,8 @@ fn chill_other_works() {
ConfigOp::Set(10),
ConfigOp::Set(10),
ConfigOp::Noop,
ConfigOp::Noop
ConfigOp::Noop,
ConfigOp::Noop,
));

// Still can't chill these users
Expand All @@ -5585,7 +5660,8 @@ fn chill_other_works() {
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Noop,
ConfigOp::Noop
ConfigOp::Noop,
ConfigOp::Noop,
));

// Still can't chill these users
Expand All @@ -5606,7 +5682,8 @@ fn chill_other_works() {
ConfigOp::Set(10),
ConfigOp::Set(10),
ConfigOp::Set(Percent::from_percent(75)),
ConfigOp::Noop
ConfigOp::Noop,
ConfigOp::Noop,
));

// 16 people total because tests start with 2 active one
Expand Down Expand Up @@ -5652,6 +5729,7 @@ fn capped_stakers_works() {
ConfigOp::Set(max),
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Noop,
));

// can create `max - validator_count` validators
Expand Down Expand Up @@ -5722,6 +5800,7 @@ fn capped_stakers_works() {
ConfigOp::Remove,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(last_nominator), vec![1]));
assert_ok!(Staking::validate(
Expand Down Expand Up @@ -5757,6 +5836,7 @@ fn min_commission_works() {
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Set(Perbill::from_percent(10)),
ConfigOp::Noop,
));

// can't make it less than 10 now
Expand Down
Loading

0 comments on commit fde4447

Please sign in to comment.