Files
pezkuwi-sdk/bizinikiwi/pezframe/staking-async/src/tests/payout_stakers.rs
T
pezkuwichain 3139ffa25e fix: Complete snowbridge pezpallet rebrand and critical bug fixes
- snowbridge-pezpallet-* → pezsnowbridge-pezpallet-* (201 refs)
- pallet/ directories → pezpallet/ (4 locations)
- Fixed pezpallet.rs self-include recursion bug
- Fixed sc-chain-spec hardcoded crate name in derive macro
- Reverted .pezpallet_by_name() to .pallet_by_name() (subxt API)
- Added BizinikiwiConfig type alias for zombienet tests
- Deleted obsolete session state files

Verified: pezsnowbridge-pezpallet-*, pezpallet-staking,
pezpallet-staking-async, pezframe-benchmarking-cli all pass cargo check
2025-12-16 09:57:23 +03:00

1695 lines
55 KiB
Rust

// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use crate::session_rotation::Eras;
use pezframe_support::dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo};
use pezsp_runtime::{bounded_btree_map, traits::Dispatchable};
#[test]
fn rewards_with_nominator_should_work() {
ExtBuilder::default().nominate(true).session_per_era(3).build_and_execute(|| {
let init_balance_11 = asset::total_balance::<T>(&11);
let init_balance_21 = asset::total_balance::<T>(&21);
let init_balance_101 = asset::total_balance::<T>(&101);
// Set payees
Payee::<T>::insert(11, RewardDestination::Account(11));
Payee::<T>::insert(21, RewardDestination::Account(21));
Payee::<T>::insert(101, RewardDestination::Account(101));
Eras::<T>::reward_active_era(vec![(11, 50)]);
Eras::<T>::reward_active_era(vec![(11, 50)]);
// This is the second validator of the current elected set.
Eras::<T>::reward_active_era(vec![(21, 50)]);
// Compute total payout now for whole duration of the session.
let validator_payout_0 = validator_payout_for(time_per_era());
let maximum_payout = total_payout_for(time_per_era());
assert_eq_uvec!(Session::validators(), vec![11, 21]);
assert_eq!(asset::total_balance::<T>(&11), init_balance_11);
assert_eq!(asset::total_balance::<T>(&21), init_balance_21);
assert_eq!(asset::total_balance::<T>(&101), init_balance_101);
assert_eq!(
ErasRewardPoints::<T>::get(active_era()),
EraRewardPoints { total: 50 * 3, individual: bounded_btree_map![11 => 100, 21 => 50] }
);
let part_for_11 = Perbill::from_rational::<u32>(1000, 1250);
let part_for_21 = Perbill::from_rational::<u32>(1000, 1250);
let part_for_101_from_11 = Perbill::from_rational::<u32>(250, 1250);
let part_for_101_from_21 = Perbill::from_rational::<u32>(250, 1250);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid {
era_index: 1,
validator_payout: validator_payout_0,
remainder: maximum_payout - validator_payout_0
},
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
assert_eq!(mock::RewardRemainderUnbalanced::get(), maximum_payout - validator_payout_0);
// make note of total issuance before rewards.
let pre_issuance = asset::total_issuance::<T>();
mock::make_all_reward_payment(1);
assert_eq!(
mock::staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Account(11), amount: 4000 },
Event::Rewarded { stash: 101, dest: RewardDestination::Account(101), amount: 1000 },
Event::PayoutStarted { era_index: 1, validator_stash: 21, page: 0, next: None },
Event::Rewarded { stash: 21, dest: RewardDestination::Account(21), amount: 2000 },
Event::Rewarded { stash: 101, dest: RewardDestination::Account(101), amount: 500 }
]
);
// total issuance should have increased
let post_issuance = asset::total_issuance::<T>();
assert_eq!(post_issuance, pre_issuance + validator_payout_0);
assert_eq_error_rate!(
asset::total_balance::<T>(&11),
init_balance_11 + part_for_11 * validator_payout_0 * 2 / 3,
2,
);
assert_eq_error_rate!(
asset::total_balance::<T>(&21),
init_balance_21 + part_for_21 * validator_payout_0 * 1 / 3,
2,
);
assert_eq_error_rate!(
asset::total_balance::<T>(&101),
init_balance_101 +
part_for_101_from_11 * validator_payout_0 * 2 / 3 +
part_for_101_from_21 * validator_payout_0 * 1 / 3,
2
);
assert_eq_uvec!(Session::validators(), vec![11, 21]);
Eras::<T>::reward_active_era(vec![(11, 1)]);
// Compute total payout now for whole duration as other parameter won't change
let total_payout_1 = validator_payout_for(time_per_era());
Session::roll_until_active_era(3);
assert_eq!(
mock::RewardRemainderUnbalanced::get(),
maximum_payout * 2 - validator_payout_0 - total_payout_1,
);
assert_eq!(
mock::staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 7, active_era: 2, planned_era: 3 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 8, active_era: 2, planned_era: 3 },
Event::EraPaid { era_index: 2, validator_payout: 7500, remainder: 7500 },
Event::SessionRotated { starting_session: 9, active_era: 3, planned_era: 3 }
]
);
mock::make_all_reward_payment(2);
assert_eq!(asset::total_issuance::<T>(), post_issuance + total_payout_1);
assert_eq_error_rate!(
asset::total_balance::<T>(&11),
init_balance_11 + part_for_11 * (validator_payout_0 * 2 / 3 + total_payout_1),
2,
);
assert_eq_error_rate!(
asset::total_balance::<T>(&21),
init_balance_21 + part_for_21 * validator_payout_0 * 1 / 3,
2,
);
assert_eq_error_rate!(
asset::total_balance::<T>(&101),
init_balance_101 +
part_for_101_from_11 * (validator_payout_0 * 2 / 3 + total_payout_1) +
part_for_101_from_21 * validator_payout_0 * 1 / 3,
2
);
});
}
#[test]
fn rewards_no_nominator_should_work() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
assert_eq_uvec!(Session::validators(), vec![11, 21]);
// with no backers
assert_eq_uvec!(
era_exposures(1),
vec![
(11, Exposure::<AccountId, Balance> { total: 1000, own: 1000, others: vec![] }),
(21, Exposure::<AccountId, Balance> { total: 1000, own: 1000, others: vec![] })
]
);
// give them some points
reward_all_elected();
// go to next active era
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
// payout era 1
make_all_reward_payment(1);
// payout works
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Staked, amount: 3750 },
Event::PayoutStarted { era_index: 1, validator_stash: 21, page: 0, next: None },
Event::Rewarded { stash: 21, dest: RewardDestination::Staked, amount: 3750 }
]
);
});
}
#[test]
fn nominating_and_rewards_should_work() {
ExtBuilder::default()
.nominate(false)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
// initial validators, note that 41 has more stake than 11
assert_eq_uvec!(Session::validators(), vec![41, 21]);
// bond two monitors, both favouring 11
bond_nominator(1, 5000, vec![11, 41]);
bond_virtual_nominator(3, 333, 5000, vec![11]);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::Bonded { stash: 1, amount: 5000 },
Event::Bonded { stash: 3, amount: 5000 },
]
);
// reward our two winning validators
Eras::<T>::reward_active_era(vec![(41, 1)]);
Eras::<T>::reward_active_era(vec![(21, 1)]);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 7500 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
// 11 now has more votes
assert_eq_uvec!(Session::validators(), vec![11, 41]);
assert_eq!(ErasStakersPaged::<T>::iter_prefix_values((active_era(),)).count(), 2);
assert_eq!(
Staking::eras_stakers(active_era(), &11),
Exposure {
total: 7500,
own: 1000,
others: vec![
IndividualExposure { who: 1, value: 1500 },
IndividualExposure { who: 3, value: 5000 }
]
}
);
assert_eq!(
Staking::eras_stakers(active_era(), &41),
Exposure {
total: 7500,
own: 4000,
others: vec![IndividualExposure { who: 1, value: 3500 }]
}
);
// payout era 1, in which 21 and 41 were validators with no nominators.
mock::make_all_reward_payment(1);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 21, page: 0, next: None },
Event::Rewarded { stash: 21, dest: RewardDestination::Staked, amount: 3750 },
Event::PayoutStarted { era_index: 1, validator_stash: 41, page: 0, next: None },
Event::Rewarded { stash: 41, dest: RewardDestination::Staked, amount: 3750 }
]
);
reward_all_elected();
Session::roll_until_active_era(3);
// ignore session rotation events, we've seen them before.
let _ = staking_events_since_last_call();
// for era 2 we had a nominator too, who is rewarded.
mock::make_all_reward_payment(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 2, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Staked, amount: 500 },
Event::Rewarded { stash: 1, dest: RewardDestination::Stash, amount: 750 },
Event::Rewarded {
stash: 3,
dest: RewardDestination::Account(333),
amount: 2500
},
Event::PayoutStarted { era_index: 2, validator_stash: 41, page: 0, next: None },
Event::Rewarded { stash: 41, dest: RewardDestination::Staked, amount: 2000 },
Event::Rewarded { stash: 1, dest: RewardDestination::Stash, amount: 1750 }
]
);
});
}
#[test]
fn reward_destination_staked() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// initial conditions
assert!(Session::validators().contains(&11));
assert_eq!(Staking::payee(11.into()), Some(RewardDestination::Staked));
assert_eq!(asset::total_balance::<T>(&11), 1001);
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 1000,
unlocking: Default::default(),
}
);
// reward era 1 and payout at era 2
Eras::<T>::reward_active_era(vec![(11, 1)]);
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
mock::make_all_reward_payment(1);
assert_eq!(ClaimedRewards::<T>::get(1, &11), vec![0]);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Staked, amount: 7500 }
]
);
// ledger must have been increased
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 8500,
active: 8500,
unlocking: Default::default(),
}
);
// balance also updated
assert_eq!(asset::total_balance::<T>(&11), 1001 + 7500);
});
}
#[test]
fn reward_to_stake_works() {
ExtBuilder::default()
.nominate(false)
.set_status(31, StakerStatus::Idle)
.set_status(41, StakerStatus::Idle)
.set_stake(21, 2000)
.try_state(false)
.build_and_execute(|| {
assert_eq!(ValidatorCount::<T>::get(), 2);
// Confirm account 10 and 20 are validators
assert!(<Validators<T>>::contains_key(&11) && <Validators<T>>::contains_key(&21));
assert_eq!(Staking::eras_stakers(active_era(), &11).total, 1000);
assert_eq!(Staking::eras_stakers(active_era(), &21).total, 2000);
// Give the man some money.
let _ = asset::set_stakeable_balance::<T>(&10, 1000);
let _ = asset::set_stakeable_balance::<T>(&20, 1000);
// Bypass logic and change current exposure
Eras::<T>::upsert_exposure(0, &21, Exposure { total: 69, own: 69, others: vec![] });
<Ledger<T>>::insert(
&20,
StakingLedgerInspect {
stash: 21,
total: 69,
active: 69,
unlocking: Default::default(),
},
);
// Compute total payout now for whole duration as other parameter won't change
let validator_payout_0 = validator_payout_for(time_per_era());
Pezpallet::<T>::reward_by_ids(vec![(11, 1)]);
Pezpallet::<T>::reward_by_ids(vec![(21, 1)]);
// New era --> rewards are paid --> stakes are changed
Session::roll_until_active_era(2);
make_all_reward_payment(1);
assert_eq!(Staking::eras_stakers(active_era(), &11).total, 1000);
assert_eq!(Staking::eras_stakers(active_era(), &21).total, 2000);
let _11_balance = asset::stakeable_balance::<T>(&11);
assert_eq!(_11_balance, 1000 + validator_payout_0 / 2);
// Trigger another new era as the info are frozen before the era start.
Session::roll_until_active_era(3);
// -- new infos
assert_eq!(
Staking::eras_stakers(active_era(), &11).total,
1000 + validator_payout_0 / 2
);
assert_eq!(
Staking::eras_stakers(active_era(), &21).total,
2000 + validator_payout_0 / 2
);
});
}
#[test]
fn reward_destination_stash() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// initial conditions
assert!(Session::validators().contains(&11));
assert_ok!(Staking::set_payee(RuntimeOrigin::signed(11), RewardDestination::Stash));
assert_eq!(asset::total_balance::<T>(&11), 1001);
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 1000,
unlocking: Default::default(),
}
);
// reward era 1 and payout at era 2
Eras::<T>::reward_active_era(vec![(11, 1)]);
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
mock::make_all_reward_payment(1);
assert_eq!(ClaimedRewards::<T>::get(1, &11), vec![0]);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Stash, amount: 7500 }
]
);
// ledger same, balance increased
assert_eq!(asset::total_balance::<T>(&11), 1001 + 7500);
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 1000,
unlocking: Default::default(),
}
);
});
}
#[test]
fn reward_destination_account() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// initial conditions
assert!(Session::validators().contains(&11));
assert_ok!(Staking::set_payee(RuntimeOrigin::signed(11), RewardDestination::Account(7)));
assert_eq!(asset::total_balance::<T>(&11), 1001);
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 1000,
unlocking: Default::default(),
}
);
// reward era 1 and payout at era 2
Eras::<T>::reward_active_era(vec![(11, 1)]);
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
mock::make_all_reward_payment(1);
assert_eq!(ClaimedRewards::<T>::get(1, &11), vec![0]);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Account(7), amount: 7500 }
]
);
// balance and ledger the same, 7 is unded
assert_eq!(asset::total_balance::<T>(&11), 1001);
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 1000,
unlocking: Default::default(),
}
);
assert_eq!(asset::total_balance::<T>(&7), 7500);
});
}
#[test]
fn validator_prefs_no_commission() {
ExtBuilder::default().build_and_execute(|| {
Eras::<T>::reward_active_era(vec![(11, 1)]);
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
mock::make_all_reward_payment(1);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Staked, amount: 6000 },
Event::Rewarded { stash: 101, dest: RewardDestination::Staked, amount: 1500 }
]
);
});
}
#[test]
fn validator_prefs_100_commission() {
ExtBuilder::default().build_and_execute(|| {
let commission = Perbill::from_percent(100);
Eras::<T>::reward_active_era(vec![(11, 1)]);
Eras::<T>::set_validator_prefs(1, &11, ValidatorPrefs { commission, ..Default::default() });
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
mock::make_all_reward_payment(1);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Staked, amount: 7500 }
]
);
});
}
#[test]
fn validator_payment_some_commission_prefs_work() {
ExtBuilder::default().build_and_execute(|| {
let commission = Perbill::from_percent(40);
Eras::<T>::reward_active_era(vec![(11, 1)]);
Eras::<T>::set_validator_prefs(1, &11, ValidatorPrefs { commission, ..Default::default() });
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
mock::make_all_reward_payment(1);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Staked, amount: 6600 },
Event::Rewarded { stash: 101, dest: RewardDestination::Staked, amount: 900 }
]
);
});
}
#[test]
fn min_commission_works() {
ExtBuilder::default().build_and_execute(|| {
// account 11 controls the stash of itself.
assert_ok!(Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(5), blocked: false }
));
// event emitted should be correct
assert_eq!(
*staking_events().last().unwrap(),
Event::ValidatorPrefsSet {
stash: 11,
prefs: ValidatorPrefs { commission: Perbill::from_percent(5), blocked: false }
}
);
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Set(Perbill::from_percent(10)),
ConfigOp::Noop,
));
// can't make it less than 10 now
assert_noop!(
Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(5), blocked: false }
),
Error::<T>::CommissionTooLow
);
// can only change to higher.
assert_ok!(Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(10), blocked: false }
));
assert_ok!(Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(15), blocked: false }
));
})
}
#[test]
fn set_min_commission_works_with_admin_origin() {
ExtBuilder::default().build_and_execute(|| {
// no minimum commission set initially
assert_eq!(MinCommission::<T>::get(), Zero::zero());
// root can set min commission
assert_ok!(Staking::set_min_commission(RuntimeOrigin::root(), Perbill::from_percent(10)));
assert_eq!(MinCommission::<T>::get(), Perbill::from_percent(10));
// Non privileged origin can not set min_commission
assert_noop!(
Staking::set_min_commission(RuntimeOrigin::signed(2), Perbill::from_percent(15)),
BadOrigin
);
// Admin Origin can set min commission
assert_ok!(Staking::set_min_commission(
RuntimeOrigin::signed(1),
Perbill::from_percent(15),
));
// setting commission below min_commission fails
assert_noop!(
Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(14), blocked: false }
),
Error::<T>::CommissionTooLow
);
// setting commission >= min_commission works
assert_ok!(Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(15), blocked: false }
));
})
}
#[test]
fn force_apply_min_commission_works() {
let prefs = |c| ValidatorPrefs { commission: Perbill::from_percent(c), blocked: false };
let validators = || Validators::<T>::iter().collect::<Vec<_>>();
ExtBuilder::default().build_and_execute(|| {
assert_ok!(Staking::validate(RuntimeOrigin::signed(31), prefs(10)));
assert_ok!(Staking::validate(RuntimeOrigin::signed(21), prefs(5)));
// Given
assert_eq!(validators(), vec![(31, prefs(10)), (21, prefs(5)), (11, prefs(0))]);
MinCommission::<T>::set(Perbill::from_percent(5));
// When applying to a commission greater than min
assert_ok!(Staking::force_apply_min_commission(RuntimeOrigin::signed(1), 31));
// Then the commission is not changed
assert_eq!(validators(), vec![(31, prefs(10)), (21, prefs(5)), (11, prefs(0))]);
// When applying to a commission that is equal to min
assert_ok!(Staking::force_apply_min_commission(RuntimeOrigin::signed(1), 21));
// Then the commission is not changed
assert_eq!(validators(), vec![(31, prefs(10)), (21, prefs(5)), (11, prefs(0))]);
// When applying to a commission that is less than the min
assert_ok!(Staking::force_apply_min_commission(RuntimeOrigin::signed(1), 11));
// Then the commission is bumped to the min
assert_eq!(validators(), vec![(31, prefs(10)), (21, prefs(5)), (11, prefs(5))]);
// When applying commission to a validator that doesn't exist then storage is not altered
assert_noop!(
Staking::force_apply_min_commission(RuntimeOrigin::signed(1), 420),
Error::<T>::NotStash
);
});
}
#[test]
fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
// should check that:
// * rewards get paid until history_depth for both validators and nominators
// * an invalid era to claim doesn't update last_reward
// * double claim of one era fails
ExtBuilder::default().nominate(true).build_and_execute(|| {
// Consumed weight for all payout_stakers dispatches that fail
let err_weight = <T as Config>::WeightInfo::payout_stakers_alive_staked(0);
// Check state
Payee::<T>::insert(11, RewardDestination::Account(11));
Payee::<T>::insert(101, RewardDestination::Account(101));
// reward for era 1
Pezpallet::<T>::reward_by_ids(vec![(11, 1)]);
Session::roll_until_active_era(2);
// reward for era 2
Pezpallet::<T>::reward_by_ids(vec![(11, 1)]);
Session::roll_until_active_era(3);
// reward for era 3
Pezpallet::<T>::reward_by_ids(vec![(11, 1)]);
// go to the history depth era
Session::roll_until_active_era(HistoryDepth::get() + 1);
let _ = staking_events_since_last_call();
// Last kept is 1:
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 0),
// Fail: Era out of history
Error::<T>::InvalidEraToReward.with_weight(err_weight)
);
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0));
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 1, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Account(11), amount: 6000 },
Event::Rewarded { stash: 101, dest: RewardDestination::Account(101), amount: 1500 }
]
);
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0));
assert_eq!(
staking_events_since_last_call(),
vec![
Event::PayoutStarted { era_index: 2, validator_stash: 11, page: 0, next: None },
Event::Rewarded { stash: 11, dest: RewardDestination::Account(11), amount: 6000 },
Event::Rewarded { stash: 101, dest: RewardDestination::Account(101), amount: 1500 }
]
);
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0),
// Fail: Double claim
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, active_era(), 0),
// Fail: Era ongoing
Error::<T>::InvalidEraToReward.with_weight(err_weight)
);
});
}
#[test]
fn nominators_over_max_exposure_page_size_are_rewarded() {
ExtBuilder::default().build_and_execute(|| {
// bond one nominator more than the max exposure page size to validator 11 in era 1
for i in 0..=MaxExposurePageSize::get() {
let stash = 10_000 + i as AccountId;
let balance = 10_000 + i as Balance;
asset::set_stakeable_balance::<T>(&stash, balance);
assert_ok!(Staking::bond(
RuntimeOrigin::signed(stash),
balance,
RewardDestination::Stash
));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(stash), vec![11]));
}
// enact new staker set -- era 2
Session::roll_until_active_era(2);
// reward for era 2
Pezpallet::<T>::reward_by_ids(vec![(11, 1)]);
Session::roll_until_active_era(3);
mock::make_all_reward_payment(2);
// Assert nominators from 1 to Max are rewarded
let mut i: u32 = 0;
while i < MaxExposurePageSize::get() {
let stash = 10_000 + i as AccountId;
let balance = 10_000 + i as Balance;
assert!(asset::stakeable_balance::<T>(&stash) > balance);
i += 1;
}
// Assert overflowing nominators from page 1 are also rewarded
let stash = 10_000 + i as AccountId;
assert!(asset::stakeable_balance::<T>(&stash) > (10_000 + i) as Balance);
});
}
#[test]
fn test_nominators_are_rewarded_for_all_exposure_page() {
ExtBuilder::default().build_and_execute(|| {
// 3 pages of exposure
let nominator_count = 2 * MaxExposurePageSize::get() + 1;
for i in 0..nominator_count {
let stash = 10_000 + i as AccountId;
let balance = 10_000 + i as Balance;
asset::set_stakeable_balance::<T>(&stash, balance);
assert_ok!(Staking::bond(
RuntimeOrigin::signed(stash),
balance,
RewardDestination::Stash
));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(stash), vec![11]));
}
// enact
Session::roll_until_active_era(2);
// give rewards
Pezpallet::<T>::reward_by_ids(vec![(11, 1)]);
Session::roll_until_active_era(3);
mock::make_all_reward_payment(2);
assert_eq!(Eras::<T>::exposure_page_count(2, &11), 3);
// Assert all nominators are rewarded according to their stake
for i in 0..nominator_count {
// balance of the nominator after the reward payout.
let current_balance = asset::stakeable_balance::<T>(&((10000 + i) as AccountId));
// balance of the nominator in the previous iteration.
let previous_balance = asset::stakeable_balance::<T>(&((10000 + i - 1) as AccountId));
// balance before the reward.
let original_balance = 10_000 + i as Balance;
assert!(current_balance > original_balance);
// since the stake of the nominator is increasing for each iteration, the final balance
// after the reward should also be higher than the previous iteration.
assert!(current_balance > previous_balance);
}
});
}
#[test]
fn test_multi_page_payout_stakers_by_page() {
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;
// Create a validator:
bond_validator(11, balance); // Default(64)
assert_eq!(Validators::<T>::count(), 1);
// Create nominators, targeting stash of validators
for i in 0..100 {
let bond_amount = balance + i as Balance;
bond_nominator(1000 + i, bond_amount, vec![11]);
// with multi page reward payout, payout exposure is same as total exposure.
total_exposure += bond_amount;
}
// enact the above changes
Session::roll_until_active_era(2);
// give rewards
Staking::reward_by_ids(vec![(11, 1)]);
// 100 nominators fit into 2 pages of exposure
assert_eq!(MaxExposurePageSize::get(), 64);
assert_eq!(Eras::<T>::exposure_page_count(2, &11), 2);
// compute and ensure the reward amount is greater than zero.
let payout = validator_payout_for(time_per_era());
Session::roll_until_active_era(3);
// verify the exposures are calculated correctly.
let actual_exposure_0 = Eras::<T>::get_paged_exposure(2, &11, 0).unwrap();
assert_eq!(actual_exposure_0.total(), total_exposure);
assert_eq!(actual_exposure_0.own(), 1000);
assert_eq!(actual_exposure_0.others().len(), 64);
let actual_exposure_1 = Eras::<T>::get_paged_exposure(2, &11, 1).unwrap();
assert_eq!(actual_exposure_1.total(), total_exposure);
// own stake is only included once in the first page
assert_eq!(actual_exposure_1.own(), 0);
assert_eq!(actual_exposure_1.others().len(), 100 - 64);
let pre_payout_total_issuance = pezpallet_balances::TotalIssuance::<T>::get();
RewardOnUnbalanceWasCalled::set(false);
// flush any events
let _ = staking_events_since_last_call();
let controller_balance_before_p0_payout = asset::stakeable_balance::<T>(&11);
// Payout rewards for first exposure page
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0));
// verify `Rewarded` events are being executed
assert!(matches!(
staking_events_since_last_call().as_slice(),
&[
Event::PayoutStarted { era_index: 2, validator_stash: 11, page: 0, next: Some(1) },
..,
Event::Rewarded { stash: 1063, dest: RewardDestination::Stash, amount: _ },
Event::Rewarded { stash: 1064, dest: RewardDestination::Stash, amount: _ },
]
));
let controller_balance_after_p0_payout = asset::stakeable_balance::<T>(&11);
// verify rewards have been paid out but still some left
assert!(pezpallet_balances::TotalIssuance::<T>::get() > pre_payout_total_issuance);
assert!(pezpallet_balances::TotalIssuance::<T>::get() < pre_payout_total_issuance + payout);
// verify the validator has been rewarded
assert!(controller_balance_after_p0_payout > controller_balance_before_p0_payout);
// Payout the second and last page of nominators
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 1));
// verify `Rewarded` events are being executed for the second page.
let events = staking_events_since_last_call();
assert!(matches!(
events.as_slice(),
&[
Event::PayoutStarted { era_index: 2, validator_stash: 11, page: 1, next: None },
Event::Rewarded { stash: 1065, dest: RewardDestination::Stash, amount: _ },
Event::Rewarded { stash: 1066, dest: RewardDestination::Stash, amount: _ },
..
]
));
// verify the validator was not rewarded the second time
assert_eq!(asset::stakeable_balance::<T>(&11), controller_balance_after_p0_payout);
// verify all rewards have been paid out
assert_eq_error_rate!(
pezpallet_balances::TotalIssuance::<T>::get(),
pre_payout_total_issuance + payout,
2
);
assert!(RewardOnUnbalanceWasCalled::get());
// Top 64 nominators of validator 11 automatically paid out, including the validator
assert!(asset::stakeable_balance::<T>(&11) > balance);
for i in 0..100 {
assert!(asset::stakeable_balance::<T>(&(1000 + i)) > balance + i as Balance);
}
// verify rewards are tracked to prevent double claims
for page in 0..Eras::<T>::exposure_page_count(2, &11) {
assert_eq!(Eras::<T>::is_rewards_claimed(2, &11, page), true);
}
for i in 4..17 {
Staking::reward_by_ids(vec![(11, 1)]);
// compute and ensure the reward amount is greater than zero.
let payout = validator_payout_for(time_per_era());
let pre_payout_total_issuance = pezpallet_balances::TotalIssuance::<T>::get();
Session::roll_until_active_era(i);
RewardOnUnbalanceWasCalled::set(false);
mock::make_all_reward_payment(i - 1);
assert_eq_error_rate!(
pezpallet_balances::TotalIssuance::<T>::get(),
pre_payout_total_issuance + payout,
2
);
assert!(RewardOnUnbalanceWasCalled::get());
// verify we track rewards for each era and page
for page in 0..Eras::<T>::exposure_page_count(i - 1, &11) {
assert_eq!(Eras::<T>::is_rewards_claimed(i - 1, &11, page), true);
}
}
assert_eq!(ClaimedRewards::<T>::get(14, &11), vec![0, 1]);
let last_era = 99;
let history_depth = HistoryDepth::get();
let last_reward_era = last_era - 1;
let first_claimable_reward_era = last_era - history_depth;
for i in 17..=last_era {
Staking::reward_by_ids(vec![(11, 1)]);
// compute and ensure the reward amount is greater than zero.
let _ = validator_payout_for(time_per_era());
Session::roll_until_active_era(i);
}
// With manual pruning, old eras are NOT automatically cleaned up
// ClaimedRewards for old eras will remain until manually pruned
// Check eras that had rewards claimed (2-14, based on test setup)
for era in 2..15 {
// These eras had rewards and should still have ClaimedRewards since pruning is manual
assert!(!ClaimedRewards::<T>::get(era, &11).is_empty());
}
// verify only page 0 is marked as claimed
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
first_claimable_reward_era,
0
));
assert_eq!(ClaimedRewards::<T>::get(first_claimable_reward_era, &11), vec![0]);
// verify page 0 and 1 are marked as claimed
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
first_claimable_reward_era,
1
));
assert_eq!(ClaimedRewards::<T>::get(first_claimable_reward_era, &11), vec![0, 1]);
// verify only page 0 is marked as claimed
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
last_reward_era,
0
));
assert_eq!(ClaimedRewards::<T>::get(last_reward_era, &11), vec![0]);
// verify page 0 and 1 are marked as claimed
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
last_reward_era,
1
));
assert_eq!(ClaimedRewards::<T>::get(last_reward_era, &11), vec![0, 1]);
// Out of order claims works.
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 69, 0));
assert_eq!(ClaimedRewards::<T>::get(69, &11), vec![0]);
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 23, 1));
assert_eq!(ClaimedRewards::<T>::get(23, &11), vec![1]);
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 42, 0));
assert_eq!(ClaimedRewards::<T>::get(42, &11), vec![0]);
});
}
#[test]
fn test_multi_page_payout_stakers_backward_compatible() {
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;
// Create a validator:
bond_validator(11, balance); // Default(64)
assert_eq!(Validators::<T>::count(), 1);
let err_weight = <T as Config>::WeightInfo::payout_stakers_alive_staked(0);
// Create nominators, targeting stash of validators
for i in 0..100 {
let bond_amount = balance + i as Balance;
bond_nominator(1000 + i, bond_amount, vec![11]);
// with multi page reward payout, payout exposure is same as total exposure.
total_exposure += bond_amount;
}
Session::roll_until_active_era(2);
Staking::reward_by_ids(vec![(11, 1)]);
// Since `MaxExposurePageSize = 64`, there are two pages of validator exposure.
assert_eq!(Eras::<T>::exposure_page_count(2, &11), 2);
// compute and ensure the reward amount is greater than zero.
let payout = validator_payout_for(time_per_era());
Session::roll_until_active_era(3);
// verify the exposures are calculated correctly.
let actual_exposure_0 = Eras::<T>::get_paged_exposure(2, &11, 0).unwrap();
assert_eq!(actual_exposure_0.total(), total_exposure);
assert_eq!(actual_exposure_0.own(), 1000);
assert_eq!(actual_exposure_0.others().len(), 64);
let actual_exposure_1 = Eras::<T>::get_paged_exposure(2, &11, 1).unwrap();
assert_eq!(actual_exposure_1.total(), total_exposure);
// own stake is only included once in the first page
assert_eq!(actual_exposure_1.own(), 0);
assert_eq!(actual_exposure_1.others().len(), 100 - 64);
let pre_payout_total_issuance = pezpallet_balances::TotalIssuance::<T>::get();
RewardOnUnbalanceWasCalled::set(false);
let controller_balance_before_p0_payout = asset::stakeable_balance::<T>(&11);
// Payout rewards for first exposure page
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 2));
// page 0 is claimed
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
let controller_balance_after_p0_payout = asset::stakeable_balance::<T>(&11);
// verify rewards have been paid out but still some left
assert!(pezpallet_balances::TotalIssuance::<T>::get() > pre_payout_total_issuance);
assert!(pezpallet_balances::TotalIssuance::<T>::get() < pre_payout_total_issuance + payout);
// verify the validator has been rewarded
assert!(controller_balance_after_p0_payout > controller_balance_before_p0_payout);
// This should payout the second and last page of nominators
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 2));
// cannot claim any more pages
assert_noop!(
Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 2),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
// verify the validator was not rewarded the second time
assert_eq!(asset::stakeable_balance::<T>(&11), controller_balance_after_p0_payout);
// verify all rewards have been paid out
assert_eq_error_rate!(
pezpallet_balances::TotalIssuance::<T>::get(),
pre_payout_total_issuance + payout,
2
);
assert!(RewardOnUnbalanceWasCalled::get());
// verify all nominators of validator 11 are paid out, including the validator
// Validator payout goes to controller.
assert!(asset::stakeable_balance::<T>(&11) > balance);
for i in 0..100 {
assert!(asset::stakeable_balance::<T>(&(1000 + i)) > balance + i as Balance);
}
// verify rewards are tracked to prevent double claims
for page in 0..Eras::<T>::exposure_page_count(2, &11) {
assert_eq!(Eras::<T>::is_rewards_claimed(2, &11, page), true);
}
for i in 4..17 {
Staking::reward_by_ids(vec![(11, 1)]);
// compute and ensure the reward amount is greater than zero.
let payout = validator_payout_for(time_per_era());
let pre_payout_total_issuance = pezpallet_balances::TotalIssuance::<T>::get();
Session::roll_until_active_era(i);
RewardOnUnbalanceWasCalled::set(false);
mock::make_all_reward_payment(i - 1);
assert_eq_error_rate!(
pezpallet_balances::TotalIssuance::<T>::get(),
pre_payout_total_issuance + payout,
2
);
assert!(RewardOnUnbalanceWasCalled::get());
// verify we track rewards for each era and page
for page in 0..Eras::<T>::exposure_page_count(i - 1, &11) {
assert_eq!(Eras::<T>::is_rewards_claimed(i - 1, &11, page), true);
}
}
assert_eq!(ClaimedRewards::<T>::get(14, &11), vec![0, 1]);
let last_era = 99;
let history_depth = HistoryDepth::get();
let last_reward_era = last_era - 1;
let first_claimable_reward_era = last_era - history_depth;
for i in 17..=last_era {
Staking::reward_by_ids(vec![(11, 1)]);
// compute and ensure the reward amount is greater than zero.
let _ = validator_payout_for(time_per_era());
Session::roll_until_active_era(i);
}
// With manual pruning, old eras are NOT automatically cleaned up
// ClaimedRewards for old eras will remain until manually pruned
// Check eras that had rewards claimed (2-14, based on test setup)
for era in 2..15 {
// These eras had rewards and should still have ClaimedRewards since pruning is manual
assert!(!ClaimedRewards::<T>::get(era, &11).is_empty());
}
// verify only page 0 is marked as claimed
assert_ok!(Staking::payout_stakers(
RuntimeOrigin::signed(1337),
11,
first_claimable_reward_era
));
assert_eq!(ClaimedRewards::<T>::get(first_claimable_reward_era, &11), vec![0]);
// verify page 0 and 1 are marked as claimed
assert_ok!(Staking::payout_stakers(
RuntimeOrigin::signed(1337),
11,
first_claimable_reward_era,
));
assert_eq!(ClaimedRewards::<T>::get(first_claimable_reward_era, &11), vec![0, 1]);
// change order and verify only page 1 is marked as claimed
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
last_reward_era,
1
));
assert_eq!(ClaimedRewards::<T>::get(last_reward_era, &11), vec![1]);
// verify page 0 is claimed even when explicit page is not passed
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, last_reward_era,));
assert_eq!(ClaimedRewards::<T>::get(last_reward_era, &11), vec![1, 0]);
// cannot claim any more pages
assert_noop!(
Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, last_reward_era),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
// Create 4 nominator pages
for i in 100..200 {
let bond_amount = balance + i as Balance;
bond_nominator(1000 + i, bond_amount, vec![11]);
}
let test_era = last_era + 1;
Session::roll_until_active_era(test_era);
Staking::reward_by_ids(vec![(11, 1)]);
// compute and ensure the reward amount is greater than zero.
let _ = validator_payout_for(time_per_era());
Session::roll_until_active_era(test_era + 1);
// Out of order claims works.
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, test_era, 2));
assert_eq!(ClaimedRewards::<T>::get(test_era, &11), vec![2]);
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, test_era));
assert_eq!(ClaimedRewards::<T>::get(test_era, &11), vec![2, 0]);
// cannot claim page 2 again
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, test_era, 2),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, test_era));
assert_eq!(ClaimedRewards::<T>::get(test_era, &11), vec![2, 0, 1]);
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, test_era));
assert_eq!(ClaimedRewards::<T>::get(test_era, &11), vec![2, 0, 1, 3]);
});
}
#[test]
fn test_page_count_and_size() {
ExtBuilder::default().has_stakers(false).build_and_execute(|| {
let balance = 1000;
// Track the exposure of the validator and all nominators.
// Create a validator:
bond_validator(11, balance); // Default(64)
assert_eq!(Validators::<T>::count(), 1);
// Create nominators, targeting stash of validators
for i in 0..100 {
let bond_amount = balance + i as Balance;
bond_nominator(1000 + i, bond_amount, vec![11]);
}
Session::roll_until_active_era(2);
// Since max exposure page size is 64, 2 pages of nominators are created.
assert_eq!(MaxExposurePageSize::get(), 64);
assert_eq!(Eras::<T>::exposure_page_count(2, &11), 2);
// first page has 64 nominators
assert_eq!(Eras::<T>::get_paged_exposure(2, &11, 0).unwrap().others().len(), 64);
// second page has 36 nominators
assert_eq!(Eras::<T>::get_paged_exposure(2, &11, 1).unwrap().others().len(), 36);
// now lets decrease page size
MaxExposurePageSize::set(32);
Session::roll_until_active_era(3);
// now we expect 4 pages.
assert_eq!(Eras::<T>::exposure_page_count(3, &11), 4);
// first 3 pages have 32 nominators each
assert_eq!(Eras::<T>::get_paged_exposure(3, &11, 0).unwrap().others().len(), 32);
assert_eq!(Eras::<T>::get_paged_exposure(3, &11, 1).unwrap().others().len(), 32);
assert_eq!(Eras::<T>::get_paged_exposure(3, &11, 2).unwrap().others().len(), 32);
assert_eq!(Eras::<T>::get_paged_exposure(3, &11, 3).unwrap().others().len(), 4);
// now lets decrease page size even more
MaxExposurePageSize::set(5);
Session::roll_until_active_era(4);
// now we expect the max 20 pages (100/5).
assert_eq!(Eras::<T>::exposure_page_count(4, &11), 20);
});
}
#[test]
fn payout_stakers_handles_basic_errors() {
ExtBuilder::default().has_stakers(false).build_and_execute(|| {
let err_weight = <T as Config>::WeightInfo::payout_stakers_alive_staked(0);
// Same setup as the test above
let balance = 1000;
bond_validator(11, balance); // Default(64)
// Create nominators, targeting stash
for i in 0..100 {
bond_nominator(1000 + i, balance + i as Balance, vec![11]);
}
Session::roll_until_active_era(2);
Staking::reward_by_ids(vec![(11, 1)]);
let _ = validator_payout_for(time_per_era());
Session::roll_until_active_era(3);
// Wrong Era, too big
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 3, 0),
Error::<T>::InvalidEraToReward.with_weight(err_weight)
);
// Wrong Staker
assert_noop!(
Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 10, 2, 0),
Error::<T>::NotStash.with_weight(err_weight)
);
let last_era = 99;
for i in 4..=last_era {
Staking::reward_by_ids(vec![(11, 1)]);
// compute and ensure the reward amount is greater than zero.
let _ = validator_payout_for(time_per_era());
Session::roll_until_active_era(i);
}
let history_depth = HistoryDepth::get();
let expected_last_reward_era = last_era - 1;
let expected_start_reward_era = last_era - history_depth;
// We are at era last_era=99. Given history_depth=80, we should be able
// to payout era starting from expected_start_reward_era=19 through
// expected_last_reward_era=98 (80 total eras), but not 18 or 99.
assert_noop!(
Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_start_reward_era - 1,
0
),
Error::<T>::InvalidEraToReward.with_weight(err_weight)
);
assert_noop!(
Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_last_reward_era + 1,
0
),
Error::<T>::InvalidEraToReward.with_weight(err_weight)
);
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_start_reward_era,
0
));
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_last_reward_era,
0
));
// can call page 1
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_last_reward_era,
1
));
// Can't claim again
assert_noop!(
Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_start_reward_era,
0
),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
assert_noop!(
Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_last_reward_era,
0
),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
assert_noop!(
Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_last_reward_era,
1
),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
// invalid page
assert_noop!(
Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
11,
expected_last_reward_era,
2
),
Error::<T>::InvalidPage.with_weight(err_weight)
);
});
}
#[test]
fn test_commission_paid_across_pages() {
ExtBuilder::default().has_stakers(false).build_and_execute(|| {
let balance = 1;
let commission = 50;
// Create a validator:
bond_validator(11, balance);
assert_ok!(Staking::validate(
RuntimeOrigin::signed(11),
ValidatorPrefs { commission: Perbill::from_percent(commission), blocked: false }
));
assert_eq!(Validators::<T>::count(), 1);
// Create nominators, targeting stash of validators
for i in 0..200 {
let bond_amount = balance + i as Balance;
bond_nominator(1000 + i, bond_amount, vec![11]);
}
Session::roll_until_active_era(2);
Staking::reward_by_ids(vec![(11, 1)]);
// Since `MaxExposurePageSize = 64`, there are four pages of validator
// exposure.
assert_eq!(Eras::<T>::exposure_page_count(2, &11), 4);
// compute and ensure the reward amount is greater than zero.
let payout = validator_payout_for(time_per_era());
Session::roll_until_active_era(3);
let initial_balance = asset::stakeable_balance::<T>(&11);
// Payout rewards for first exposure page
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0));
let controller_balance_after_p0_payout = asset::stakeable_balance::<T>(&11);
// some commission is paid
assert!(initial_balance < controller_balance_after_p0_payout);
// payout all pages
for i in 1..4 {
let before_balance = asset::stakeable_balance::<T>(&11);
assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, i));
let after_balance = asset::stakeable_balance::<T>(&11);
// some commission is paid for every page
assert!(before_balance < after_balance);
}
assert_eq_error_rate!(asset::stakeable_balance::<T>(&11), initial_balance + payout / 2, 1,);
});
}
#[test]
fn payout_stakers_handles_weight_refund() {
// Note: this test relies on the assumption that `payout_stakers_alive_staked` is solely used by
// `payout_stakers` to calculate the weight of each payout op.
ExtBuilder::default().has_stakers(false).build_and_execute(|| {
use crate::Call as StakingCall;
let max_nom_rewarded = MaxExposurePageSize::get();
// Make sure the configured value is meaningful for our use.
assert!(max_nom_rewarded >= 4);
let half_max_nom_rewarded = max_nom_rewarded / 2;
// Sanity check our max and half max nominator quantities.
assert!(half_max_nom_rewarded > 0);
assert!(max_nom_rewarded > half_max_nom_rewarded);
let max_nom_rewarded_weight =
<T as Config>::WeightInfo::payout_stakers_alive_staked(max_nom_rewarded);
let half_max_nom_rewarded_weight =
<T as Config>::WeightInfo::payout_stakers_alive_staked(half_max_nom_rewarded);
let zero_nom_payouts_weight = <T as Config>::WeightInfo::payout_stakers_alive_staked(0);
assert!(zero_nom_payouts_weight.any_gt(Weight::zero()));
assert!(half_max_nom_rewarded_weight.any_gt(zero_nom_payouts_weight));
assert!(max_nom_rewarded_weight.any_gt(half_max_nom_rewarded_weight));
let balance = 1000;
bond_validator(11, balance);
// Era 2
Session::roll_until_active_era(2);
// Reward just the validator.
Staking::reward_by_ids(vec![(11, 1)]);
// Add some `half_max_nom_rewarded` nominators who will start backing the validator in the
// next era.
for i in 0..half_max_nom_rewarded {
bond_nominator((1000 + i).into(), balance + i as Balance, vec![11]);
}
// Era 3
Session::roll_until_active_era(3);
// Collect payouts when there are no nominators
let call = RuntimeCall::Staking(StakingCall::payout_stakers_by_page {
validator_stash: 11,
era: 2,
page: 0,
});
let info = call.get_dispatch_info();
let result = call.dispatch(RuntimeOrigin::signed(20));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), zero_nom_payouts_weight);
// The validator is not rewarded in this era; so there will be zero payouts to claim for
// this era.
// next era -- with nominators now
Session::roll_until_active_era(4);
// Collect payouts for an era where the validator did not receive any points.
let call = RuntimeCall::Staking(StakingCall::payout_stakers_by_page {
validator_stash: 11,
era: 3,
page: 0,
});
let info = call.get_dispatch_info();
let result = call.dispatch(RuntimeOrigin::signed(20));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), zero_nom_payouts_weight);
// Reward the validator and its nominators.
Staking::reward_by_ids(vec![(11, 1)]);
// Era 5
Session::roll_until_active_era(5);
// Collect payouts when the validator has `half_max_nom_rewarded` nominators.
let call = RuntimeCall::Staking(StakingCall::payout_stakers_by_page {
validator_stash: 11,
era: 4,
page: 0,
});
let info = call.get_dispatch_info();
let result = call.dispatch(RuntimeOrigin::signed(20));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), half_max_nom_rewarded_weight);
// Add enough nominators so that we are at the limit. They will be active nominators
// in the next era.
for i in half_max_nom_rewarded..max_nom_rewarded {
bond_nominator((1000 + i).into(), balance + i as Balance, vec![11]);
}
// Era 6
Session::roll_until_active_era(6);
// We now have `max_nom_rewarded` nominators actively nominating our validator.
// Reward the validator so we can collect for everyone in the next era.
Staking::reward_by_ids(vec![(11, 1)]);
// Era 7
Session::roll_until_active_era(7);
// Collect payouts when the validator had `half_max_nom_rewarded` nominators.
let call = RuntimeCall::Staking(StakingCall::payout_stakers_by_page {
validator_stash: 11,
era: 6,
page: 0,
});
let info = call.get_dispatch_info();
let result = call.dispatch(RuntimeOrigin::signed(20));
assert_ok!(result);
assert_eq!(extract_actual_weight(&result, &info), max_nom_rewarded_weight);
// Try and collect payouts for an era that has already been collected.
let call = RuntimeCall::Staking(StakingCall::payout_stakers_by_page {
validator_stash: 11,
era: 6,
page: 0,
});
let info = call.get_dispatch_info();
let result = call.dispatch(RuntimeOrigin::signed(20));
assert!(result.is_err());
// When there is an error the consumed weight == weight when there are 0 nominator payouts.
assert_eq!(extract_actual_weight(&result, &info), zero_nom_payouts_weight);
});
}
#[test]
fn test_runtime_api_pending_rewards() {
ExtBuilder::default().build_and_execute(|| {
// GIVEN
let err_weight = <T as Config>::WeightInfo::payout_stakers_alive_staked(0);
let stake = 100;
// validator with non-paged exposure, rewards marked in legacy claimed rewards.
let validator_one = 301;
// validator with non-paged exposure, rewards marked in paged claimed rewards.
let validator_two = 302;
// validator with paged exposure.
let validator_three = 303;
// Set staker
for v in validator_one..=validator_three {
let _ = asset::set_stakeable_balance::<T>(&v, stake);
assert_ok!(Staking::bond(RuntimeOrigin::signed(v), stake, RewardDestination::Staked));
}
// Add reward points
let reward = EraRewardPoints {
total: 1,
individual: bounded_btree_map![validator_one => 1, validator_two => 1, validator_three => 1],
};
ErasRewardPoints::<T>::insert(0, reward);
// build exposure
let mut individual_exposures: Vec<IndividualExposure<AccountId, Balance>> = vec![];
for i in 0..=MaxExposurePageSize::get() {
individual_exposures.push(IndividualExposure { who: i.into(), value: stake });
}
let exposure = Exposure::<AccountId, Balance> {
total: stake * (MaxExposurePageSize::get() as Balance + 2),
own: stake,
others: individual_exposures,
};
// add exposure for validators
Eras::<T>::upsert_exposure(0, &validator_one, exposure.clone());
Eras::<T>::upsert_exposure(0, &validator_two, exposure.clone());
// add some reward to be distributed
ErasValidatorReward::<T>::insert(0, 1000);
// SCENARIO: Validator with paged exposure (two pages).
// validators have not claimed rewards, so pending rewards is true.
assert!(Eras::<T>::pending_rewards(0, &validator_one));
assert!(Eras::<T>::pending_rewards(0, &validator_two));
// and payout works
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_one, 0));
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_two, 0));
// validators have two pages of exposure, so pending rewards is still true.
assert!(Eras::<T>::pending_rewards(0, &validator_one));
assert!(Eras::<T>::pending_rewards(0, &validator_two));
// payout again only for validator one
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_one, 0));
// now pending rewards is false for validator one
assert!(!Eras::<T>::pending_rewards(0, &validator_one));
// and payout fails for validator one
assert_noop!(
Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_one, 0),
Error::<T>::AlreadyClaimed.with_weight(err_weight)
);
// while pending reward is true for validator two
assert!(Eras::<T>::pending_rewards(0, &validator_two));
// and payout works again for validator two.
assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), validator_two, 0));
});
}