Skip to content
tests.rs 176 KiB
Newer Older
			ConfigOp::Remove,
			ConfigOp::Remove,
			ConfigOp::Noop,
			ConfigOp::Noop,
		assert_ok!(Staking::nominate(RuntimeOrigin::signed(last_nominator), vec![1]));
		assert_ok!(Staking::validate(
			RuntimeOrigin::signed(last_validator),
			ValidatorPrefs::default()
		));
#[test]
fn min_commission_works() {
	ExtBuilder::default().build_and_execute(|| {
		// account 10 controls the stash from account 11
		assert_ok!(Staking::validate(
			RuntimeOrigin::signed(10),
			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)),
		));

		// can't make it less than 10 now
		assert_noop!(
			Staking::validate(
				RuntimeOrigin::signed(10),
				ValidatorPrefs { commission: Perbill::from_percent(5), blocked: false }
			),
			Error::<Test>::CommissionTooLow
		);

		// can only change to higher.
		assert_ok!(Staking::validate(
			RuntimeOrigin::signed(10),
			ValidatorPrefs { commission: Perbill::from_percent(10), blocked: false }
		));

		assert_ok!(Staking::validate(
			RuntimeOrigin::signed(10),
			ValidatorPrefs { commission: Perbill::from_percent(15), blocked: false }
		));
	})
}

#[test]
fn change_of_max_nominations() {
	use frame_election_provider_support::ElectionDataProvider;
	ExtBuilder::default()
		.add_staker(60, 61, 10, StakerStatus::Nominator(vec![1]))
		.add_staker(70, 71, 10, StakerStatus::Nominator(vec![1, 2, 3]))
		.balance_factor(10)
		.build_and_execute(|| {
			// pre-condition
			assert_eq!(MaxNominations::get(), 16);

			assert_eq!(
				Nominators::<Test>::iter()
					.map(|(k, n)| (k, n.targets.len()))
					.collect::<Vec<_>>(),
				vec![(70, 3), (101, 2), (60, 1)]
			);
			// 3 validators and 3 nominators
			assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3);

			// abrupt change from 16 to 4, everyone should be fine.
			MaxNominations::set(4);

			assert_eq!(
				Nominators::<Test>::iter()
					.map(|(k, n)| (k, n.targets.len()))
					.collect::<Vec<_>>(),
				vec![(70, 3), (101, 2), (60, 1)]
			);
			assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3);

			// abrupt change from 4 to 3, everyone should be fine.
			MaxNominations::set(3);

			assert_eq!(
				Nominators::<Test>::iter()
					.map(|(k, n)| (k, n.targets.len()))
					.collect::<Vec<_>>(),
				vec![(70, 3), (101, 2), (60, 1)]
			);
			assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 3);

			// abrupt change from 3 to 2, this should cause some nominators to be non-decodable, and
			// thus non-existent unless if they update.
			MaxNominations::set(2);

			assert_eq!(
				Nominators::<Test>::iter()
					.map(|(k, n)| (k, n.targets.len()))
					.collect::<Vec<_>>(),
				vec![(101, 2), (60, 1)]
			);
			// 70 is still in storage..
			assert!(Nominators::<Test>::contains_key(70));
			// but its value cannot be decoded and default is returned.
			assert!(Nominators::<Test>::get(70).is_none());

			assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 2);
			assert!(Nominators::<Test>::contains_key(101));

			// abrupt change from 2 to 1, this should cause some nominators to be non-decodable, and
			// thus non-existent unless if they update.
			MaxNominations::set(1);

			assert_eq!(
				Nominators::<Test>::iter()
					.map(|(k, n)| (k, n.targets.len()))
					.collect::<Vec<_>>(),
				vec![(60, 1)]
			);
			assert!(Nominators::<Test>::contains_key(70));
			assert!(Nominators::<Test>::contains_key(60));
			assert!(Nominators::<Test>::get(70).is_none());
			assert!(Nominators::<Test>::get(60).is_some());
			assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 1);

			// now one of them can revive themselves by re-nominating to a proper value.
			assert_ok!(Staking::nominate(RuntimeOrigin::signed(71), vec![1]));
			assert_eq!(
				Nominators::<Test>::iter()
					.map(|(k, n)| (k, n.targets.len()))
					.collect::<Vec<_>>(),
				vec![(70, 1), (60, 1)]
			);

			// or they can be chilled by any account.
			assert!(Nominators::<Test>::contains_key(101));
			assert!(Nominators::<Test>::get(101).is_none());
			assert_ok!(Staking::chill_other(RuntimeOrigin::signed(70), 100));
			assert!(!Nominators::<Test>::contains_key(101));
			assert!(Nominators::<Test>::get(101).is_none());
		})
}

mod sorted_list_provider {
	use super::*;
	use frame_election_provider_support::SortedListProvider;

	#[test]
	fn re_nominate_does_not_change_counters_or_list() {
		ExtBuilder::default().nominate(true).build_and_execute(|| {
			// given
			let pre_insert_voter_count =
				(Nominators::<Test>::count() + Validators::<Test>::count()) as u32;
			assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);

			assert_eq!(
				<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
				vec![11, 21, 31, 101]
			);
			assert_ok!(Staking::nominate(RuntimeOrigin::signed(100), vec![41]));
			assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
			// and the list is the same
			assert_eq!(
				<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
				vec![11, 21, 31, 101]
			);
		});
	}

	#[test]
	fn re_validate_does_not_change_counters_or_list() {
		ExtBuilder::default().nominate(false).build_and_execute(|| {
			// given
			let pre_insert_voter_count =
				(Nominators::<Test>::count() + Validators::<Test>::count()) as u32;
			assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);

			assert_eq!(<Test as Config>::VoterList::iter().collect::<Vec<_>>(), vec![11, 21, 31]);

			// when account 11 re-validates
			assert_ok!(Staking::validate(RuntimeOrigin::signed(10), Default::default()));

			// then counts don't change
			assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
			assert_eq!(<Test as Config>::VoterList::iter().collect::<Vec<_>>(), vec![11, 21, 31]);

#[test]
fn force_apply_min_commission_works() {
	let prefs = |c| ValidatorPrefs { commission: Perbill::from_percent(c), blocked: false };
	let validators = || Validators::<Test>::iter().collect::<Vec<_>>();
	ExtBuilder::default().build_and_execute(|| {
		assert_ok!(Staking::validate(RuntimeOrigin::signed(30), prefs(10)));
		assert_ok!(Staking::validate(RuntimeOrigin::signed(20), prefs(5)));

		// Given
		assert_eq!(validators(), vec![(31, prefs(10)), (21, prefs(5)), (11, prefs(0))]);
		MinCommission::<Test>::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),
#[test]
fn proportional_slash_stop_slashing_if_remaining_zero() {
	let c = |era, value| UnlockChunk::<Balance> { era, value };
	// Given
	let mut ledger = StakingLedger::<Test> {
		stash: 123,
		total: 40,
		active: 20,
		// we have some chunks, but they are not affected.
		unlocking: bounded_vec![c(1, 10), c(2, 10)],
		claimed_rewards: bounded_vec![],
	};

	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> {
		stash: 123,
		total: 10,
		active: 10,
		unlocking: bounded_vec![],
		claimed_rewards: bounded_vec![],
	};
	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 pre_bonding_era_cannot_be_claimed() {
	// Verifies initial conditions of mock
	ExtBuilder::default().nominate(false).build_and_execute(|| {
		let history_depth = HistoryDepth::get();
		// jump to some era above history_depth
		let mut current_era = history_depth + 10;
		let last_reward_era = current_era - 1;
		let start_reward_era = current_era - history_depth;

		// put some money in stash=3 and controller=4.
		for i in 3..5 {
			let _ = Balances::make_free_balance_be(&i, 2000);
		}

		mock::start_active_era(current_era);

		// add a new candidate for being a validator. account 3 controlled by 4.
		assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 4, 1500, RewardDestination::Controller));

		let claimed_rewards: BoundedVec<_, _> =
			(start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap();
		assert_eq!(
			Staking::ledger(&4).unwrap(),
			StakingLedger {
				stash: 3,
				total: 1500,
				active: 1500,
				unlocking: Default::default(),
				claimed_rewards,
			}
		);

		// start next era
		current_era = current_era + 1;
		mock::start_active_era(current_era);

		// claiming reward for last era in which validator was active works
		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(4), 3, current_era - 1));

		// consumed weight for all payout_stakers dispatches that fail
		let err_weight = <Test as Config>::WeightInfo::payout_stakers_alive_staked(0);
		// cannot claim rewards for an era before bonding occured as it is
		// already marked as claimed.
		assert_noop!(
			Staking::payout_stakers(RuntimeOrigin::signed(4), 3, current_era - 2),
			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
		);

		// decoding will fail now since Staking Ledger is in corrupt state
		HistoryDepth::set(history_depth - 1);
		assert_eq!(Staking::ledger(&4), None);

		// make sure stakers still cannot claim rewards that they are not meant to
		assert_noop!(
			Staking::payout_stakers(RuntimeOrigin::signed(4), 3, current_era - 2),
			Error::<Test>::NotController
		);

		// fix the corrupted state for post conditions check
		HistoryDepth::set(history_depth);
	});
}

#[test]
	// Verifies initial conditions of mock
	ExtBuilder::default().nominate(false).build_and_execute(|| {
		let original_history_depth = HistoryDepth::get();
		let mut current_era = original_history_depth + 10;
		let last_reward_era = current_era - 1;
		let start_reward_era = current_era - original_history_depth;

		// put some money in (stash, controller)=(3,4),(5,6).
		for i in 3..7 {
			let _ = Balances::make_free_balance_be(&i, 2000);
		}

		// start current era
		mock::start_active_era(current_era);

		// add a new candidate for being a staker. account 3 controlled by 4.
		assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 4, 1500, RewardDestination::Controller));

		// all previous era before the bonding action should be marked as
		// claimed.
		let claimed_rewards: BoundedVec<_, _> =
			(start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap();
		assert_eq!(
			Staking::ledger(&4).unwrap(),
			StakingLedger {
				stash: 3,
				total: 1500,
				active: 1500,
				unlocking: Default::default(),
				claimed_rewards,
			}
		);

		// next era
		current_era = current_era + 1;
		mock::start_active_era(current_era);

		// claiming reward for last era in which validator was active works
		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(4), 3, current_era - 1));

		// next era
		current_era = current_era + 1;
		mock::start_active_era(current_era);

		// history_depth reduced without migration
		let history_depth = original_history_depth - 1;
		HistoryDepth::set(history_depth);
		// claiming reward does not work anymore
		assert_noop!(
			Staking::payout_stakers(RuntimeOrigin::signed(4), 3, current_era - 1),
			Error::<Test>::NotController
		);

		// new stakers can still bond
		assert_ok!(Staking::bond(RuntimeOrigin::signed(5), 6, 1200, RewardDestination::Controller));

		// new staking ledgers created will be bounded by the current history depth
		let last_reward_era = current_era - 1;
		let start_reward_era = current_era - history_depth;
		let claimed_rewards: BoundedVec<_, _> =
			(start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap();
		assert_eq!(
			Staking::ledger(&6).unwrap(),
			StakingLedger {
				stash: 5,
				total: 1200,
				active: 1200,
				unlocking: Default::default(),
				claimed_rewards,
			}
		);

		// fix the corrupted state for post conditions check
		HistoryDepth::set(original_history_depth);
	});
}

#[test]
fn reducing_max_unlocking_chunks_abrupt() {
	// Concern is on validators only
	// By Default 11, 10 are stash and ctrl 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), 4, 300, RewardDestination::Staked));
		assert!(matches!(Staking::ledger(4), Some(_)));

		// when staker unbonds
		assert_ok!(Staking::unbond(RuntimeOrigin::signed(4), 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(4),
			Some(StakingLedger {
				unlocking,
				..
			}) if unlocking==expected_unlocking));

		// when staker unbonds at next era
		start_active_era(11);
		assert_ok!(Staking::unbond(RuntimeOrigin::signed(4), 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(4),
			Some(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(4), 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(4), 20), Error::<Test>::NotController);
		assert_noop!(Staking::rebond(RuntimeOrigin::signed(4), 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(
				RuntimeOrigin::signed(10),
				ValidatorPrefs { commission: Perbill::from_percent(14), blocked: false }
			),
			Error::<Test>::CommissionTooLow
		);

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

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(10));
				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(10));
			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(10));

			assert_noop!(
				Staking::withdraw_unbonded(RuntimeOrigin::signed(10), 0),
				Error::<Test>::IncorrectSlashingSpans
			);

			let num_slashing_spans = Staking::slashing_spans(&11).map_or(0, |s| s.iter().count());
			assert_ok!(Staking::withdraw_unbonded(
				RuntimeOrigin::signed(10),
				num_slashing_spans as u32
			));
		});
	}

	#[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());
		})
	}