Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements a % cap on staking rewards from era inflation #1660

Merged
merged 28 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e88d2cd
Adds minimum treasury inflation per era
gpestana Sep 21, 2023
8a64bef
Adds set_min_treasury_fraction extrinsic; Improves tests
gpestana Sep 22, 2023
810e9d8
Updates docs
gpestana Sep 22, 2023
02d5512
refactors so that the max_stakers_payout is used rather than min_rema…
gpestana Sep 23, 2023
9ad6e10
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Sep 23, 2023
046119d
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Sep 23, 2023
6cc5785
Addresses PR comments
gpestana Sep 23, 2023
59fb828
fix westend weights
gpestana Sep 23, 2023
cf89d26
Defines backwards compatible default for MaxStakedRewards
gpestana Sep 25, 2023
910c785
Merge branch 'master' into gpestana/403-treasury_mint_remainder
gpestana Oct 23, 2023
50f12ad
bubbles up cap of staking rewards to staking pallet
gpestana Oct 23, 2023
d98148f
Backtracks changes to era_payout in runtime-common
gpestana Oct 23, 2023
019c04a
improves tests
gpestana Oct 23, 2023
261450c
More test improvements
gpestana Oct 23, 2023
c317d43
Merge branch 'master' into gpestana/403-treasury_mint_remainder
gpestana Jan 8, 2024
13a0941
Fixes benchmarks
gpestana Jan 8, 2024
b1d3780
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jan 8, 2024
078bf86
".git/.scripts/commands/bench/bench.sh" --subcommand=runtime --runtim…
Jan 8, 2024
3d9a364
Merge branch 'master' into gpestana/403-treasury_mint_remainder
gpestana Jan 21, 2024
1fdf817
Add prdoc
gpestana Jan 21, 2024
9a18ae1
Fixes prdoc
gpestana Jan 22, 2024
06c9eb8
sets max staked rewards through the set_staking_config callable
gpestana Jan 28, 2024
48eab32
fixes nom pools e2e tests set_staking_configs
gpestana Jan 28, 2024
b3cb0d9
removes set_max_staked_rewards from westend weights
gpestana Jan 28, 2024
f9f4e94
fixes benchmarks
gpestana Jan 28, 2024
4f589b3
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jan 28, 2024
73adb6c
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jan 28, 2024
72d5f3c
Merge branch 'master' into gpestana/403-treasury_mint_remainder
gpestana Feb 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading