diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 09809483ec..324a367f48 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -237,6 +237,7 @@ const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] = parameter_types! { pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS; pub static MaxNominations: u32 = 16; + pub static RewardOnUnbalanceWasCalled: bool = false; } impl pallet_bags_list::Config for Test { @@ -255,6 +256,13 @@ impl onchain::Config for OnChainSeqPhragmen { type WeightInfo = (); } +pub struct MockReward {} +impl OnUnbalanced> for MockReward { + fn on_unbalanced(_: PositiveImbalanceOf) { + RewardOnUnbalanceWasCalled::set(true); + } +} + impl crate::pallet::pallet::Config for Test { type MaxNominations = MaxNominations; type Currency = Balances; @@ -263,7 +271,7 @@ impl crate::pallet::pallet::Config for Test { type RewardRemainder = RewardRemainderMock; type Event = Event; type Slash = (); - type Reward = (); + type Reward = MockReward; type SessionsPerEra = SessionsPerEra; type SlashDeferDuration = SlashDeferDuration; type SlashCancelOrigin = frame_system::EnsureRoot; diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 90f19c6bad..96af7009be 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -174,11 +174,13 @@ impl Pallet { Self::deposit_event(Event::::PayoutStarted(era, ledger.stash.clone())); + let mut total_imbalance = PositiveImbalanceOf::::zero(); // We can now make total validator payout: if let Some(imbalance) = Self::make_payout(&ledger.stash, validator_staking_payout + validator_commission_payout) { Self::deposit_event(Event::::Rewarded(ledger.stash, imbalance.peek())); + total_imbalance.subsume(imbalance); } // Track the number of payout ops to nominators. Note: @@ -199,9 +201,11 @@ impl Pallet { nominator_payout_count += 1; let e = Event::::Rewarded(nominator.who.clone(), imbalance.peek()); Self::deposit_event(e); + total_imbalance.subsume(imbalance); } } + T::Reward::on_unbalanced(total_imbalance); debug_assert!(nominator_payout_count <= T::MaxNominatorRewardedPerValidator::get()); Ok(Some(T::WeightInfo::payout_stakers_alive_staked(nominator_payout_count)).into()) } diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 1a8fd59d23..d24509033e 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -121,6 +121,8 @@ pub mod pallet { type Slash: OnUnbalanced>; /// Handler for the unbalanced increment when rewarding a staker. + /// NOTE: in most cases, the implementation of `OnUnbalanced` should modify the total + /// issuance. type Reward: OnUnbalanced>; /// Number of sessions per era. diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 7161418afc..f0de26b588 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3370,26 +3370,47 @@ fn set_history_depth_works() { #[test] fn test_payout_stakers() { - // Here we will test validator can set `max_nominators_payout` and it works. - // We also test that `payout_extra_nominators` works. + // Test that payout_stakers work in general, including that only the top + // `T::MaxNominatorRewardedPerValidator` nominators are rewarded. ExtBuilder::default().has_stakers(false).build_and_execute(|| { let balance = 1000; + // Track the exposure of the validator and all nominators. + let mut total_exposure = balance; + // Track the exposure of the validator and the nominators that will get paid out. + let mut payout_exposure = balance; // Create a validator: bond_validator(11, 10, balance); // Default(64) + assert_eq!(Validators::::count(), 1); // Create nominators, targeting stash of validators for i in 0..100 { - bond_nominator(1000 + i, 100 + i, balance + i as Balance, vec![11]); + let bond_amount = balance + i as Balance; + bond_nominator(1000 + i, 100 + i, bond_amount, vec![11]); + total_exposure += bond_amount; + if i >= 36 { + payout_exposure += bond_amount; + }; } + let payout_exposure_part = Perbill::from_rational(payout_exposure, total_exposure); mock::start_active_era(1); Staking::reward_by_ids(vec![(11, 1)]); // compute and ensure the reward amount is greater than zero. - let _ = current_total_payout_for_duration(reward_time_per_era()); + let payout = current_total_payout_for_duration(reward_time_per_era()); + let actual_paid_out = payout_exposure_part * payout; mock::start_active_era(2); + + let pre_payout_total_issuance = Balances::total_issuance(); + RewardOnUnbalanceWasCalled::set(false); assert_ok!(Staking::payout_stakers(Origin::signed(1337), 11, 1)); + assert_eq_error_rate!( + Balances::total_issuance(), + pre_payout_total_issuance + actual_paid_out, + 1 + ); + assert!(RewardOnUnbalanceWasCalled::get()); // Top 64 nominators of validator 11 automatically paid out, including the validator // Validator payout goes to controller. @@ -3418,10 +3439,19 @@ fn test_payout_stakers() { Staking::reward_by_ids(vec![(11, 1)]); // compute and ensure the reward amount is greater than zero. - let _ = current_total_payout_for_duration(reward_time_per_era()); + let payout = current_total_payout_for_duration(reward_time_per_era()); + let actual_paid_out = payout_exposure_part * payout; + let pre_payout_total_issuance = Balances::total_issuance(); mock::start_active_era(i); + RewardOnUnbalanceWasCalled::set(false); assert_ok!(Staking::payout_stakers(Origin::signed(1337), 11, i - 1)); + assert_eq_error_rate!( + Balances::total_issuance(), + pre_payout_total_issuance + actual_paid_out, + 1 + ); + assert!(RewardOnUnbalanceWasCalled::get()); } // We track rewards in `claimed_rewards` vec