Skip to content
tests.rs 218 KiB
Newer Older
#[test]
fn proportional_slash_stop_slashing_if_remaining_zero() {
	let c = |era, value| UnlockChunk::<Balance> { era, value };

	// we have some chunks, but they are not affected.
	let unlocking = bounded_vec![c(1, 10), c(2, 10)];

	let mut ledger = StakingLedger::<Test>::new(123, 20);
	ledger.total = 40;
	ledger.unlocking = unlocking;

	assert_eq!(BondingDuration::get(), 3);

	// should not slash more than the amount requested, by accidentally slashing the first chunk.
	assert_eq!(ledger.slash(18, 1, 0), 18);
}

fn proportional_ledger_slash_works() {
	let c = |era, value| UnlockChunk::<Balance> { era, value };
	// Given
	let mut ledger = StakingLedger::<Test>::new(123, 10);
	assert_eq!(BondingDuration::get(), 3);

	// When we slash a ledger with no unlocking chunks
	assert_eq!(ledger.slash(5, 1, 0), 5);
	// Then
	assert_eq!(ledger.total, 5);
	assert_eq!(ledger.active, 5);
	assert_eq!(LedgerSlashPerEra::get().0, 5);
	assert_eq!(LedgerSlashPerEra::get().1, Default::default());

	// When we slash a ledger with no unlocking chunks and the slash amount is greater then the
	// total
	assert_eq!(ledger.slash(11, 1, 0), 5);
	// Then
	assert_eq!(ledger.total, 0);
	assert_eq!(ledger.active, 0);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, Default::default());

	// Given
	ledger.unlocking = bounded_vec![c(4, 10), c(5, 10)];
	ledger.total = 2 * 10;
	ledger.active = 0;
	// When all the chunks overlap with the slash eras
	assert_eq!(ledger.slash(20, 0, 0), 20);
	// Then
	assert_eq!(ledger.unlocking, vec![]);
	assert_eq!(ledger.total, 0);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(4, 0), (5, 0)]));

	// Given
	ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
	ledger.total = 4 * 100;
	ledger.active = 0;
	// When the first 2 chunks don't overlap with the affected range of unlock eras.
	assert_eq!(ledger.slash(140, 0, 3), 140);
	// Then
	assert_eq!(ledger.unlocking, vec![c(4, 100), c(5, 100), c(6, 30), c(7, 30)]);
	assert_eq!(ledger.total, 4 * 100 - 140);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(6, 30), (7, 30)]));

	// Given
	ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
	ledger.total = 4 * 100;
	ledger.active = 0;
	// When the first 2 chunks don't overlap with the affected range of unlock eras.
	assert_eq!(ledger.slash(15, 0, 3), 15);
	// Then
	assert_eq!(ledger.unlocking, vec![c(4, 100), c(5, 100), c(6, 100 - 8), c(7, 100 - 7)]);
	assert_eq!(ledger.total, 4 * 100 - 15);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(6, 92), (7, 93)]));

	// Given
	ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
	ledger.active = 500;
	// 900
	ledger.total = 40 + 10 + 100 + 250 + 500;
	// When we have a partial slash that touches all chunks
	assert_eq!(ledger.slash(900 / 2, 0, 0), 450);
	// Then
	assert_eq!(ledger.active, 500 / 2);
	assert_eq!(ledger.unlocking, vec![c(4, 40 / 2), c(5, 100 / 2), c(6, 10 / 2), c(7, 250 / 2)]);
	assert_eq!(ledger.total, 900 / 2);
	assert_eq!(LedgerSlashPerEra::get().0, 500 / 2);
	assert_eq!(
		LedgerSlashPerEra::get().1,
		BTreeMap::from([(4, 40 / 2), (5, 100 / 2), (6, 10 / 2), (7, 250 / 2)])
	);

	// slash 1/4th with not chunk.
	ledger.unlocking = bounded_vec![];
	ledger.active = 500;
	ledger.total = 500;
	// When we have a partial slash that touches all chunks
	assert_eq!(ledger.slash(500 / 4, 0, 0), 500 / 4);
	// Then
	assert_eq!(ledger.active, 3 * 500 / 4);
	assert_eq!(ledger.unlocking, vec![]);
	assert_eq!(ledger.total, ledger.active);
	assert_eq!(LedgerSlashPerEra::get().0, 3 * 500 / 4);
	assert_eq!(LedgerSlashPerEra::get().1, Default::default());

	// Given we have the same as above,
	ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
	ledger.active = 500;
	ledger.total = 40 + 10 + 100 + 250 + 500; // 900
	assert_eq!(ledger.total, 900);
	// When we have a higher min balance
	assert_eq!(
		ledger.slash(
			900 / 2,
			25, /* min balance - chunks with era 0 & 2 will be slashed to <=25, causing it to
			     * get swept */
			0
		),
	);
	assert_eq!(ledger.active, 500 / 2);
	// the last chunk was not slashed 50% like all the rest, because some other earlier chunks got
	// dusted.
	assert_eq!(ledger.unlocking, vec![c(5, 100 / 2), c(7, 150)]);
	assert_eq!(ledger.total, 900 / 2);
	assert_eq!(LedgerSlashPerEra::get().0, 500 / 2);
	assert_eq!(
		LedgerSlashPerEra::get().1,
		BTreeMap::from([(4, 0), (5, 100 / 2), (6, 0), (7, 150)])
	);

	// Given
	// slash order --------------------NA--------2----------0----------1----
	ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
	ledger.active = 500;
	ledger.total = 40 + 10 + 100 + 250 + 500; // 900
	assert_eq!(
		ledger.slash(
			500 + 10 + 250 + 100 / 2, // active + era 6 + era 7 + era 5 / 2
			0,
			3 /* slash era 6 first, so the affected parts are era 6, era 7 and
			   * ledge.active. This will cause the affected to go to zero, and then we will
			   * start slashing older chunks */
		),
		500 + 250 + 10 + 100 / 2
	);
	// Then
	assert_eq!(ledger.active, 0);
	assert_eq!(ledger.unlocking, vec![c(4, 40), c(5, 100 / 2)]);
	assert_eq!(ledger.total, 90);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(5, 100 / 2), (6, 0), (7, 0)]));

	// Given
	// iteration order------------------NA---------2----------0----------1----
	ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
	ledger.active = 100;
	ledger.total = 5 * 100;
	// When
	assert_eq!(
		ledger.slash(
			351, // active + era 6 + era 7 + era 5 / 2 + 1
			50,  // min balance - everything slashed below 50 will get dusted
			3    /* slash era 3+3 first, so the affected parts are era 6, era 7 and
			      * ledge.active. This will cause the affected to go to zero, and then we will
			      * start slashing older chunks */
		),
		400
	);
	// Then
	assert_eq!(ledger.active, 0);
	assert_eq!(ledger.unlocking, vec![c(4, 100)]);
	assert_eq!(ledger.total, 100);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(5, 0), (6, 0), (7, 0)]));

	// Tests for saturating arithmetic

	// Given
	let slash = u64::MAX as Balance * 2;
	// The value of the other parts of ledger that will get slashed
	let value = slash - (10 * 4);

	ledger.active = 10;
	ledger.unlocking = bounded_vec![c(4, 10), c(5, 10), c(6, 10), c(7, value)];
	ledger.total = value + 40;
	// When
	let slash_amount = ledger.slash(slash, 0, 0);
	assert_eq_error_rate!(slash_amount, slash, 5);
	// Then
	assert_eq!(ledger.active, 0); // slash of 9
	assert_eq!(ledger.unlocking, vec![]);
	assert_eq!(ledger.total, 0);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(4, 0), (5, 0), (6, 0), (7, 0)]));

	// Given
	let slash = u64::MAX as Balance * 2;
	let value = u64::MAX as Balance * 2;
	let unit = 100;
	// slash * value that will saturate
	assert!(slash.checked_mul(value).is_none());
	// but slash * unit won't.
	assert!(slash.checked_mul(unit).is_some());
	ledger.unlocking = bounded_vec![c(4, unit), c(5, value), c(6, unit), c(7, unit)];
	//--------------------------------------note value^^^
	ledger.active = unit;
	ledger.total = unit * 4 + value;
	// When
	assert_eq!(ledger.slash(slash, 0, 0), slash);
	// Then
	// The amount slashed out of `unit`
	let affected_balance = value + unit * 4;
	let ratio =
		Perquintill::from_rational_with_rounding(slash, affected_balance, Rounding::Up).unwrap();
	// `unit` after the slash is applied
	let unit_slashed = {
		unit - unit_slash
	};
	let value_slashed = {
		value - value_slash
	};
	assert_eq!(ledger.active, unit_slashed);
	assert_eq!(ledger.unlocking, vec![c(5, value_slashed), c(7, 32)]);
	assert_eq!(ledger.total, value_slashed + 32);
	assert_eq!(LedgerSlashPerEra::get().0, 0);
	assert_eq!(
		LedgerSlashPerEra::get().1,
		BTreeMap::from([(4, 0), (5, value_slashed), (6, 0), (7, 32)])
#[test]
fn reducing_max_unlocking_chunks_abrupt() {
	// Concern is on validators only
	// By Default 11, 10 are stash and ctlr and 21,20
	ExtBuilder::default().build_and_execute(|| {
		// given a staker at era=10 and MaxUnlockChunks set to 2
		MaxUnlockingChunks::set(2);
		start_active_era(10);
		assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 300, RewardDestination::Staked));
		assert!(matches!(Staking::ledger(3.into()), Ok(_)));
		assert_ok!(Staking::unbond(RuntimeOrigin::signed(3), 20));

		// then an unlocking chunk is added at `current_era + bonding_duration`
		// => 10 + 3 = 13
		let expected_unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks> =
			bounded_vec![UnlockChunk { value: 20 as Balance, era: 13 as EraIndex }];
		assert!(matches!(Staking::ledger(3.into()),
			Ok(StakingLedger {
				unlocking,
				..
			}) if unlocking==expected_unlocking));

		// when staker unbonds at next era
		start_active_era(11);
		assert_ok!(Staking::unbond(RuntimeOrigin::signed(3), 50));
		// then another unlock chunk is added
		let expected_unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks> =
			bounded_vec![UnlockChunk { value: 20, era: 13 }, UnlockChunk { value: 50, era: 14 }];
		assert!(matches!(Staking::ledger(3.into()),
			Ok(StakingLedger {
				unlocking,
				..
			}) if unlocking==expected_unlocking));

		// when staker unbonds further
		start_active_era(12);
		// then further unbonding not possible
		assert_noop!(Staking::unbond(RuntimeOrigin::signed(3), 20), Error::<Test>::NoMoreChunks);

		// when max unlocking chunks is reduced abruptly to a low value
		MaxUnlockingChunks::set(1);
		// then unbond, rebond ops are blocked with ledger in corrupt state
		assert_noop!(Staking::unbond(RuntimeOrigin::signed(3), 20), Error::<Test>::NotController);
		assert_noop!(Staking::rebond(RuntimeOrigin::signed(3), 100), Error::<Test>::NotController);

		// reset the ledger corruption
		MaxUnlockingChunks::set(2);
	})
}

#[test]
fn cannot_set_unsupported_validator_count() {
	ExtBuilder::default().build_and_execute(|| {
		MaxWinners::set(50);
		// set validator count works
		assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 30));
		assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 50));
		// setting validator count above 100 does not work
		assert_noop!(
			Staking::set_validator_count(RuntimeOrigin::root(), 51),
			Error::<Test>::TooManyValidators,
		);
	})
}

#[test]
fn increase_validator_count_errors() {
	ExtBuilder::default().build_and_execute(|| {
		MaxWinners::set(50);
		assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 40));

		// increase works
		assert_ok!(Staking::increase_validator_count(RuntimeOrigin::root(), 6));
		assert_eq!(ValidatorCount::<Test>::get(), 46);

		// errors
		assert_noop!(
			Staking::increase_validator_count(RuntimeOrigin::root(), 5),
			Error::<Test>::TooManyValidators,
		);
	})
}

#[test]
fn scale_validator_count_errors() {
	ExtBuilder::default().build_and_execute(|| {
		MaxWinners::set(50);
		assert_ok!(Staking::set_validator_count(RuntimeOrigin::root(), 20));

		// scale value works
		assert_ok!(Staking::scale_validator_count(
			RuntimeOrigin::root(),
			Percent::from_percent(200)
		));
		assert_eq!(ValidatorCount::<Test>::get(), 40);

		// errors
		assert_noop!(
			Staking::scale_validator_count(RuntimeOrigin::root(), Percent::from_percent(126)),
			Error::<Test>::TooManyValidators,
		);
	})
}
#[test]
fn set_min_commission_works_with_admin_origin() {
	ExtBuilder::default().build_and_execute(|| {
		// no minimum commission set initially
		assert_eq!(MinCommission::<Test>::get(), Zero::zero());

		// root can set min commission
		assert_ok!(Staking::set_min_commission(RuntimeOrigin::root(), Perbill::from_percent(10)));

		assert_eq!(MinCommission::<Test>::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(
				ValidatorPrefs { commission: Perbill::from_percent(14), blocked: false }
			),
			Error::<Test>::CommissionTooLow
		);

		// setting commission >= min_commission works
		assert_ok!(Staking::validate(
			ValidatorPrefs { commission: Perbill::from_percent(15), blocked: false }
		));
	})
}

#[test]
fn can_page_exposure() {
	let mut others: Vec<IndividualExposure<AccountId, Balance>> = vec![];
	let mut total_stake: Balance = 0;
	// 19 nominators
	for i in 1..20 {
		let individual_stake: Balance = 100 * i as Balance;
		others.push(IndividualExposure { who: i, value: individual_stake });
		total_stake += individual_stake;
	}
	let own_stake: Balance = 500;
	total_stake += own_stake;
	assert_eq!(total_stake, 19_500);
	// build full exposure set
	let exposure: Exposure<AccountId, Balance> =
		Exposure { total: total_stake, own: own_stake, others };

	// when
	let (exposure_metadata, exposure_page): (
		PagedExposureMetadata<Balance>,
		Vec<ExposurePage<AccountId, Balance>>,
	) = exposure.clone().into_pages(3);

	// then
	// 7 pages of nominators.
	assert_eq!(exposure_page.len(), 7);
	assert_eq!(exposure_metadata.page_count, 7);
	// first page stake = 100 + 200 + 300
	assert!(matches!(exposure_page[0], ExposurePage { page_total: 600, .. }));
	// second page stake = 0 + 400 + 500 + 600
	assert!(matches!(exposure_page[1], ExposurePage { page_total: 1500, .. }));
	// verify overview has the total
	assert_eq!(exposure_metadata.total, 19_500);
	// verify total stake is same as in the original exposure.
	assert_eq!(
		exposure_page.iter().map(|a| a.page_total).reduce(|a, b| a + b).unwrap(),
		19_500 - exposure_metadata.own
	);
	// verify own stake is correct
	assert_eq!(exposure_metadata.own, 500);
	// verify number of nominators are same as in the original exposure.
	assert_eq!(exposure_page.iter().map(|a| a.others.len()).reduce(|a, b| a + b).unwrap(), 19);
	assert_eq!(exposure_metadata.nominator_count, 19);
}

#[test]
fn should_retain_era_info_only_upto_history_depth() {
	ExtBuilder::default().build_and_execute(|| {
		// remove existing exposure
		Pallet::<Test>::clear_era_information(0);
		let validator_stash = 10;

		for era in 0..4 {
			ClaimedRewards::<Test>::insert(era, &validator_stash, vec![0, 1, 2]);
			for page in 0..3 {
				ErasStakersPaged::<Test>::insert(
					(era, &validator_stash, page),
					ExposurePage { page_total: 100, others: vec![] },
				);
			}
		}

		for i in 0..4 {
			// Count of entries remaining in ClaimedRewards = total - cleared_count
			assert_eq!(ClaimedRewards::<Test>::iter().count(), (4 - i));
			// 1 claimed_rewards entry for each era
			assert_eq!(ClaimedRewards::<Test>::iter_prefix(i as EraIndex).count(), 1);
			// 3 entries (pages) for each era
			assert_eq!(ErasStakersPaged::<Test>::iter_prefix((i as EraIndex,)).count(), 3);

			// when clear era info
			Pallet::<Test>::clear_era_information(i as EraIndex);

			// then all era entries are cleared
			assert_eq!(ClaimedRewards::<Test>::iter_prefix(i as EraIndex).count(), 0);
			assert_eq!(ErasStakersPaged::<Test>::iter_prefix((i as EraIndex,)).count(), 0);
		}
	});
}

#[test]
fn test_legacy_claimed_rewards_is_checked_at_reward_payout() {
	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
		// Create a validator:
		bond_validator(11, 1000);

		// reward validator for next 2 eras
		mock::start_active_era(1);
		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
		mock::start_active_era(2);
		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
		mock::start_active_era(3);

		//verify rewards are not claimed
		assert_eq!(
			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
				1,
				Staking::ledger(11.into()).as_ref().unwrap(),
				&11,
				0
			),
			false
		);
		assert_eq!(
			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
				2,
				Staking::ledger(11.into()).as_ref().unwrap(),
				&11,
				0
			),
			false
		);

		// assume reward claim for era 1 was stored in legacy storage
		Ledger::<Test>::insert(
			11,
			StakingLedgerInspect {
				stash: 11,
				total: 1000,
				active: 1000,
				unlocking: Default::default(),
				legacy_claimed_rewards: bounded_vec![1],
			},
		);

		// verify rewards for era 1 cannot be claimed
		assert_noop!(
			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0),
			Error::<Test>::AlreadyClaimed
				.with_weight(<Test as Config>::WeightInfo::payout_stakers_alive_staked(0)),
		);
		assert_eq!(
			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
				1,
				Staking::ledger(11.into()).as_ref().unwrap(),
				&11,
				0
			),
			true
		);

		// verify rewards for era 2 can be claimed
		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0));
		assert_eq!(
			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
				2,
				Staking::ledger(11.into()).as_ref().unwrap(),
				&11,
				0
			),
			true
		);
		// but the new claimed rewards for era 2 is not stored in legacy storage
		assert_eq!(
			Ledger::<Test>::get(11).unwrap(),
			StakingLedgerInspect {
				stash: 11,
				total: 1000,
				active: 1000,
				unlocking: Default::default(),
				legacy_claimed_rewards: bounded_vec![1],
			},
		);
		// instead it is kept in `ClaimedRewards`
		assert_eq!(ClaimedRewards::<Test>::get(2, 11), vec![0]);
	});
}

#[test]
fn test_validator_exposure_is_backward_compatible_with_non_paged_rewards_payout() {
	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
		// case 1: exposure exist in clipped.
		// set page cap to 10
		MaxExposurePageSize::set(10);
		bond_validator(11, 1000);
		let mut expected_individual_exposures: Vec<IndividualExposure<AccountId, Balance>> = vec![];
		let mut total_exposure: Balance = 0;
		// 1st exposure page
		for i in 0..10 {
			let who = 1000 + i;
			let value = 1000 + i as Balance;
			bond_nominator(who, value, vec![11]);
			expected_individual_exposures.push(IndividualExposure { who, value });
			total_exposure += value;
		}

		for i in 10..15 {
			let who = 1000 + i;
			let value = 1000 + i as Balance;
			bond_nominator(who, value, vec![11]);
			expected_individual_exposures.push(IndividualExposure { who, value });
			total_exposure += value;
		}

		mock::start_active_era(1);
		// reward validator for current era
		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);

		// start new era
		mock::start_active_era(2);
		// verify exposure for era 1 is stored in paged storage, that each exposure is stored in
		// one and only one page, and no exposure is repeated.
		let actual_exposure_page_0 = ErasStakersPaged::<Test>::get((1, 11, 0)).unwrap();
		let actual_exposure_page_1 = ErasStakersPaged::<Test>::get((1, 11, 1)).unwrap();
		expected_individual_exposures.iter().for_each(|exposure| {
			assert!(
				actual_exposure_page_0.others.contains(exposure) ||
					actual_exposure_page_1.others.contains(exposure)
			);
		});
		assert_eq!(
			expected_individual_exposures.len(),
			actual_exposure_page_0.others.len() + actual_exposure_page_1.others.len()
		);
		// verify `EraInfo` returns page from paged storage
		assert_eq!(
			EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap().others(),
			&actual_exposure_page_0.others
		);
		assert_eq!(
			EraInfo::<Test>::get_paged_exposure(1, &11, 1).unwrap().others(),
			&actual_exposure_page_1.others
		);
		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 2);

		// validator is exposed
		assert!(<Staking as sp_staking::StakingInterface>::is_exposed_in_era(&11, &1));
		// nominators are exposed
		for i in 10..15 {
			let who: AccountId = 1000 + i;
			assert!(<Staking as sp_staking::StakingInterface>::is_exposed_in_era(&who, &1));
		}

		// case 2: exposure exist in ErasStakers and ErasStakersClipped (legacy).
		// delete paged storage and add exposure to clipped storage
		<ErasStakersPaged<Test>>::remove((1, 11, 0));
		<ErasStakersPaged<Test>>::remove((1, 11, 1));
		<ErasStakersOverview<Test>>::remove(1, 11);

		<ErasStakers<Test>>::insert(
			1,
			11,
			Exposure {
				total: total_exposure,
				own: 1000,
				others: expected_individual_exposures.clone(),
			},
		);
		let mut clipped_exposure = expected_individual_exposures.clone();
		clipped_exposure.sort_by(|a, b| b.who.cmp(&a.who));
		clipped_exposure.truncate(10);
		<ErasStakersClipped<Test>>::insert(
			1,
			11,
			Exposure { total: total_exposure, own: 1000, others: clipped_exposure.clone() },
		);

		// verify `EraInfo` returns exposure from clipped storage
		let actual_exposure_paged = EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap();
		assert_eq!(actual_exposure_paged.others(), &clipped_exposure);
		assert_eq!(actual_exposure_paged.own(), 1000);
		assert_eq!(actual_exposure_paged.exposure_metadata.page_count, 1);

		let actual_exposure_full = EraInfo::<Test>::get_full_exposure(1, &11);
		assert_eq!(actual_exposure_full.others, expected_individual_exposures);
		assert_eq!(actual_exposure_full.own, 1000);
		assert_eq!(actual_exposure_full.total, total_exposure);

		// validator is exposed
		assert!(<Staking as sp_staking::StakingInterface>::is_exposed_in_era(&11, &1));
		// nominators are exposed
		for i in 10..15 {
			let who: AccountId = 1000 + i;
			assert!(<Staking as sp_staking::StakingInterface>::is_exposed_in_era(&who, &1));
		}

		// for pages other than 0, clipped storage returns empty exposure
		assert_eq!(EraInfo::<Test>::get_paged_exposure(1, &11, 1), None);
		// page size is 1 for clipped storage
		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 1);

		// payout for page 0 works
		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 0));
		// payout for page 1 fails
		assert_noop!(
			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 1),
			Error::<Test>::InvalidPage
				.with_weight(<Test as Config>::WeightInfo::payout_stakers_alive_staked(0))
		);
	});
}

mod staking_interface {
	use frame_support::storage::with_storage_layer;
	use sp_staking::StakingInterface;

	use super::*;

	#[test]
	fn force_unstake_with_slash_works() {
		ExtBuilder::default().build_and_execute(|| {
			// without slash
			let _ = with_storage_layer::<(), _, _>(|| {
				// bond an account, can unstake
				assert_eq!(Staking::bonded(&11), Some(11));
				assert_ok!(<Staking as StakingInterface>::force_unstake(11));
				Err(DispatchError::from("revert"))
			});

			// bond again and add a slash, still can unstake.
			assert_eq!(Staking::bonded(&11), Some(11));
			add_slash(&11);
			assert_ok!(<Staking as StakingInterface>::force_unstake(11));
		});
	}

	#[test]
	fn do_withdraw_unbonded_with_wrong_slash_spans_works_as_expected() {
		ExtBuilder::default().build_and_execute(|| {
			on_offence_now(
				&[OffenceDetails {
					offender: (11, Staking::eras_stakers(active_era(), &11)),
					reporters: vec![],
				}],
				&[Perbill::from_percent(100)],
			);

			assert_eq!(Staking::bonded(&11), Some(11));
				Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0),
				Error::<Test>::IncorrectSlashingSpans
			);

			let num_slashing_spans = Staking::slashing_spans(&11).map_or(0, |s| s.iter().count());
			assert_ok!(Staking::withdraw_unbonded(

	#[test]
	fn status() {
		ExtBuilder::default().build_and_execute(|| {
			// stash of a validator is identified as a validator
			assert_eq!(Staking::status(&11).unwrap(), StakerStatus::Validator);
			// .. but not the controller.
			assert!(Staking::status(&10).is_err());

			// stash of nominator is identified as a nominator
			assert_eq!(Staking::status(&101).unwrap(), StakerStatus::Nominator(vec![11, 21]));
			// .. but not the controller.
			assert!(Staking::status(&100).is_err());

			// stash of chilled is identified as a chilled
			assert_eq!(Staking::status(&41).unwrap(), StakerStatus::Idle);
			// .. but not the controller.
			assert!(Staking::status(&40).is_err());

			// random other account.
			assert!(Staking::status(&42).is_err());
		})
	}

mod ledger {
	use super::*;

	#[test]
	fn paired_account_works() {
		ExtBuilder::default().try_state(false).build_and_execute(|| {
			assert_ok!(Staking::bond(
				RuntimeOrigin::signed(10),
				100,
				RewardDestination::Account(10)
			));

			assert_eq!(<Bonded<Test>>::get(&10), Some(10));
			assert_eq!(
				StakingLedger::<Test>::paired_account(StakingAccount::Controller(10)),
				Some(10)
			);
			assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(10)), Some(10));

			assert_eq!(<Bonded<Test>>::get(&42), None);
			assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Controller(42)), None);
			assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(42)), None);

			// bond manually stash with different controller. This is deprecated but the migration
			// has not been complete yet (controller: 100, stash: 200)
			assert_ok!(bond_controller_stash(100, 200));
			assert_eq!(<Bonded<Test>>::get(&200), Some(100));
			assert_eq!(
				StakingLedger::<Test>::paired_account(StakingAccount::Controller(100)),
				Some(200)
			);
			assert_eq!(
				StakingLedger::<Test>::paired_account(StakingAccount::Stash(200)),
				Some(100)
			);
		})
	}

	#[test]
	fn get_ledger_works() {
		ExtBuilder::default().try_state(false).build_and_execute(|| {
			// stash does not exist
			assert!(StakingLedger::<Test>::get(StakingAccount::Stash(42)).is_err());

			// bonded and paired
			assert_eq!(<Bonded<Test>>::get(&11), Some(11));

			match StakingLedger::<Test>::get(StakingAccount::Stash(11)) {
				Ok(ledger) => {
					assert_eq!(ledger.controller(), Some(11));
					assert_eq!(ledger.stash, 11);
				},
				Err(_) => panic!("staking ledger must exist"),
			};

			// bond manually stash with different controller. This is deprecated but the migration
			// has not been complete yet (controller: 100, stash: 200)
			assert_ok!(bond_controller_stash(100, 200));
			assert_eq!(<Bonded<Test>>::get(&200), Some(100));

			match StakingLedger::<Test>::get(StakingAccount::Stash(200)) {
				Ok(ledger) => {
					assert_eq!(ledger.controller(), Some(100));
					assert_eq!(ledger.stash, 200);
				},
				Err(_) => panic!("staking ledger must exist"),
			};

			match StakingLedger::<Test>::get(StakingAccount::Controller(100)) {
				Ok(ledger) => {
					assert_eq!(ledger.controller(), Some(100));
					assert_eq!(ledger.stash, 200);
				},
				Err(_) => panic!("staking ledger must exist"),
			};
		})
	}

	#[test]
	fn bond_works() {
		ExtBuilder::default().build_and_execute(|| {
			assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42)));
			assert!(<Bonded<Test>>::get(&42).is_none());

			let mut ledger: StakingLedger<Test> = StakingLedger::default_from(42);
			let reward_dest = RewardDestination::Account(10);

			assert_ok!(ledger.clone().bond(reward_dest));
			assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42)));
			assert!(<Bonded<Test>>::get(&42).is_some());
			assert_eq!(<Payee<Test>>::get(&42), Some(reward_dest));

			// cannot bond again.
			assert!(ledger.clone().bond(reward_dest).is_err());

			// once bonded, update works as expected.
			ledger.legacy_claimed_rewards = bounded_vec![1];
			assert_ok!(ledger.update());
		})
	}

	#[test]
	fn is_bonded_works() {
		ExtBuilder::default().build_and_execute(|| {
			assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42)));
			assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Controller(42)));

			// adds entry to Bonded without Ledger pair (should not happen).
			<Bonded<Test>>::insert(42, 42);
			assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Controller(42)));

			assert_eq!(<Bonded<Test>>::get(&11), Some(11));
			assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Stash(11)));
			assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Controller(11)));

			<Bonded<Test>>::remove(42); // ensures try-state checks pass.
		})
	}

	#[test]
	#[allow(deprecated)]
	fn set_payee_errors_on_controller_destination() {
		ExtBuilder::default().build_and_execute(|| {
			Payee::<Test>::insert(11, RewardDestination::Staked);
			assert_noop!(
				Staking::set_payee(RuntimeOrigin::signed(11), RewardDestination::Controller),
				Error::<Test>::ControllerDeprecated
			);
			assert_eq!(Payee::<Test>::get(&11), Some(RewardDestination::Staked));
		})
	}

	#[test]
	#[allow(deprecated)]
	fn update_payee_migration_works() {
		ExtBuilder::default().build_and_execute(|| {
			// migrate a `Controller` variant to `Account` variant.
			Payee::<Test>::insert(11, RewardDestination::Controller);
			assert_eq!(Payee::<Test>::get(&11), Some(RewardDestination::Controller));
			assert_ok!(Staking::update_payee(RuntimeOrigin::signed(11), 11));
			assert_eq!(Payee::<Test>::get(&11), Some(RewardDestination::Account(11)));

			// Do not migrate a variant if not `Controller`.
			Payee::<Test>::insert(21, RewardDestination::Stash);
			assert_eq!(Payee::<Test>::get(&21), Some(RewardDestination::Stash));
			assert_noop!(
				Staking::update_payee(RuntimeOrigin::signed(11), 21),
				Error::<Test>::NotController
			);
			assert_eq!(Payee::<Test>::get(&21), Some(RewardDestination::Stash));

	#[test]
	fn deprecate_controller_batch_works_full_weight() {
		ExtBuilder::default().build_and_execute(|| {
			// Given:

			let start = 1001;
			let mut controllers: Vec<_> = vec![];
			for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
				let ctlr: u64 = n.into();
				let stash: u64 = (n + 10000).into();

				Ledger::<Test>::insert(
					ctlr,
					StakingLedger {
						controller: None,
						total: (10 + ctlr).into(),
						active: (10 + ctlr).into(),
						..StakingLedger::default_from(stash)
					},
				);
				Bonded::<Test>::insert(stash, ctlr);
				Payee::<Test>::insert(stash, RewardDestination::Staked);

				controllers.push(ctlr);
			}

			// When:

			let bounded_controllers: BoundedVec<
				_,
				<Test as Config>::MaxControllersInDeprecationBatch,
			> = BoundedVec::try_from(controllers).unwrap();

			// Only `AdminOrigin` can sign.
			assert_noop!(
				Staking::deprecate_controller_batch(
					RuntimeOrigin::signed(2),
					bounded_controllers.clone()
				),
				BadOrigin
			);

			let result =
				Staking::deprecate_controller_batch(RuntimeOrigin::root(), bounded_controllers);
			assert_ok!(result);
			assert_eq!(
				result.unwrap().actual_weight.unwrap(),
				<Test as Config>::WeightInfo::deprecate_controller_batch(
					<Test as Config>::MaxControllersInDeprecationBatch::get()
				)
			);

			// Then:

			for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
				let ctlr: u64 = n.into();
				let stash: u64 = (n + 10000).into();

				// Ledger no longer keyed by controller.
				assert_eq!(Ledger::<Test>::get(ctlr), None);
				// Bonded now maps to the stash.
				assert_eq!(Bonded::<Test>::get(stash), Some(stash));

				// Ledger is now keyed by stash.
				let ledger_updated = Ledger::<Test>::get(stash).unwrap();
				assert_eq!(ledger_updated.stash, stash);

				// Check `active` and `total` values match the original ledger set by controller.
				assert_eq!(ledger_updated.active, (10 + ctlr).into());
				assert_eq!(ledger_updated.total, (10 + ctlr).into());
			}
		})
	}

	#[test]
	fn deprecate_controller_batch_works_half_weight() {
		ExtBuilder::default().build_and_execute(|| {
			// Given:

			let start = 1001;
			let mut controllers: Vec<_> = vec![];
			for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
				let ctlr: u64 = n.into();

				// Only half of entries are unique pairs.
				let stash: u64 = if n % 2 == 0 { (n + 10000).into() } else { ctlr };

				Ledger::<Test>::insert(
					ctlr,
					StakingLedger { controller: None, ..StakingLedger::default_from(stash) },
				);
				Bonded::<Test>::insert(stash, ctlr);
				Payee::<Test>::insert(stash, RewardDestination::Staked);