From 8c52a2dae69bae3849bfaf08c1eefd3c7f7d4a79 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Mon, 20 Apr 2020 13:13:45 +0200 Subject: [PATCH] Pass max-total to RewardRemainder on end_era (#5697) * Pass max-total to RewardRemainder on end_era * add test and event * add doc Co-authored-by: thiolliere <gui.thiolliere@gmail.com> --- substrate/frame/staking/src/inflation.rs | 5 ++-- substrate/frame/staking/src/lib.rs | 31 +++++++++++++++++++++--- substrate/frame/staking/src/mock.rs | 27 ++++++++++++++++++++- substrate/frame/staking/src/tests.rs | 5 ++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/substrate/frame/staking/src/inflation.rs b/substrate/frame/staking/src/inflation.rs index d20741d9bc4..63d008a197c 100644 --- a/substrate/frame/staking/src/inflation.rs +++ b/substrate/frame/staking/src/inflation.rs @@ -21,10 +21,11 @@ use sp_runtime::{Perbill, traits::AtLeast32Bit, curve::PiecewiseLinear}; -/// The total payout to all validators (and their nominators) per era. +/// The total payout to all validators (and their nominators) per era and maximum payout. /// /// Defined as such: -/// `payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year` +/// `staker-payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year` +/// `maximum-payout = max_yearly_inflation * total_tokens / era_per_year` /// /// `era_duration` is expressed in millisecond. pub fn compute_total_payout<N>( diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index b6ffa9081bb..40fce5b0d44 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -172,6 +172,22 @@ //! //! ## Implementation Details //! +//! ### Era payout +//! +//! The era payout is computed using yearly inflation curve defined at +//! [`T::RewardCurve`](./trait.Trait.html#associatedtype.RewardCurve) as such: +//! +//! ```nocompile +//! staker_payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year +//! ``` +//! This payout is used to reward stakers as defined in next section +//! +//! ```nocompile +//! remaining_payout = max_yearly_inflation * total_tokens / era_per_year - staker_payout +//! ``` +//! The remaining reward is send to the configurable end-point +//! [`T::RewardRemainder`](./trait.Trait.html#associatedtype.RewardRemainder). +//! //! ### Reward Calculation //! //! Validators and nominators are rewarded at the end of each era. The total reward of an era is @@ -744,6 +760,7 @@ pub trait Trait: frame_system::Trait { type CurrencyToVote: Convert<BalanceOf<Self>, VoteWeight> + Convert<u128, BalanceOf<Self>>; /// Tokens have been minted and are unused for validator-reward. + /// See [Era payout](./index.html#era-payout). type RewardRemainder: OnUnbalanced<NegativeImbalanceOf<Self>>; /// The overarching event type. @@ -772,7 +789,8 @@ pub trait Trait: frame_system::Trait { /// Interface for interacting with a session module. type SessionInterface: self::SessionInterface<Self::AccountId>; - /// The NPoS reward curve to use. + /// The NPoS reward curve used to define yearly inflation. + /// See [Era payout](./index.html#era-payout). type RewardCurve: Get<&'static PiecewiseLinear<'static>>; /// Something that can estimate the next session change, accurately or as a best effort guess. @@ -1059,6 +1077,9 @@ decl_storage! { decl_event!( pub enum Event<T> where Balance = BalanceOf<T>, <T as frame_system::Trait>::AccountId { + /// The era payout has been set; the first balance is the validator-payout; the second is + /// the remainder from the maximum amount of reward. + EraPayout(EraIndex, Balance, Balance), /// The staker has been rewarded by this amount. `AccountId` is the stash account. Reward(AccountId, Balance), /// One validator (and its nominators) has been slashed by the given amount. @@ -2570,16 +2591,20 @@ impl<T: Trait> Module<T> { let now_as_millis_u64 = T::UnixTime::now().as_millis().saturated_into::<u64>(); let era_duration = now_as_millis_u64 - active_era_start; - let (total_payout, _max_payout) = inflation::compute_total_payout( + let (validator_payout, max_payout) = inflation::compute_total_payout( &T::RewardCurve::get(), Self::eras_total_stake(&active_era.index), T::Currency::total_issuance(), // Duration of era; more than u64::MAX is rewarded as u64::MAX. era_duration.saturated_into::<u64>(), ); + let rest = max_payout.saturating_sub(validator_payout); + + Self::deposit_event(RawEvent::EraPayout(active_era.index, validator_payout, rest)); // Set ending era reward. - <ErasValidatorReward<T>>::insert(&active_era.index, total_payout); + <ErasValidatorReward<T>>::insert(&active_era.index, validator_payout); + T::RewardRemainder::on_unbalanced(T::Currency::issue(rest)); } } diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index d522a196159..20ec6f46a6b 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -277,11 +277,26 @@ parameter_types! { pub const UnsignedPriority: u64 = 1 << 20; } +thread_local! { + pub static REWARD_REMAINDER_UNBALANCED: RefCell<u128> = RefCell::new(0); +} + +pub struct RewardRemainderMock; + +impl OnUnbalanced<NegativeImbalanceOf<Test>> for RewardRemainderMock { + fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<Test>) { + REWARD_REMAINDER_UNBALANCED.with(|v| { + *v.borrow_mut() += amount.peek(); + }); + drop(amount); + } +} + impl Trait for Test { type Currency = Balances; type UnixTime = Timestamp; type CurrencyToVote = CurrencyToVoteHandler; - type RewardRemainder = (); + type RewardRemainder = RewardRemainderMock; type Event = MetaEvent; type Slash = (); type Reward = (); @@ -976,3 +991,13 @@ macro_rules! assert_session_era { ); }; } + +pub(crate) fn staking_events() -> Vec<Event<Test>> { + System::events().into_iter().map(|r| r.event).filter_map(|e| { + if let MetaEvent::staking(inner) = e { + Some(inner) + } else { + None + } + }).collect() +} diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 15afda1e3af..3920b7bc0d7 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -152,6 +152,7 @@ fn rewards_should_work() { // should check that: // * rewards get recorded per session // * rewards get paid per Era + // * `RewardRemainder::on_unbalanced` is called // * Check that nominators are also rewarded ExtBuilder::default().nominate(true).build_and_execute(|| { let init_balance_10 = Balances::total_balance(&10); @@ -197,6 +198,8 @@ fn rewards_should_work() { start_session(3); assert_eq!(Staking::active_era().unwrap().index, 1); + assert_eq!(mock::REWARD_REMAINDER_UNBALANCED.with(|v| *v.borrow()), 7050); + assert_eq!(*mock::staking_events().last().unwrap(), RawEvent::EraPayout(0, 2350, 7050)); mock::make_all_reward_payment(0); assert_eq_error_rate!(Balances::total_balance(&10), init_balance_10 + part_for_10 * total_payout_0*2/3, 2); @@ -220,6 +223,8 @@ fn rewards_should_work() { assert!(total_payout_1 > 10); // Test is meaningful if reward something mock::start_era(2); + assert_eq!(mock::REWARD_REMAINDER_UNBALANCED.with(|v| *v.borrow()), 7050*2); + assert_eq!(*mock::staking_events().last().unwrap(), RawEvent::EraPayout(1, 2350, 7050)); mock::make_all_reward_payment(1); assert_eq_error_rate!(Balances::total_balance(&10), init_balance_10 + part_for_10 * (total_payout_0 * 2/3 + total_payout_1), 2); -- GitLab