Skip to content
tests.rs 176 KiB
Newer Older
				init_balance_10 + total_payout_0 / 3,
				1
			);
			// no rewards paid to 2. This was initial election.
			assert_eq!(Balances::free_balance(2), init_balance_2);
			// reward era 2
			let total_payout_1 = current_total_payout_for_duration(reward_time_per_era());
			reward_all_elected();
Gavin Wood's avatar
Gavin Wood committed
			mock::make_all_reward_payment(1);

			assert_eq_uvec!(validator_controllers(), vec![20, 10, 2]);
			assert_eq!(Staking::eras_stakers(active_era(), 2).total, 0);
			assert_eq_error_rate!(
				Balances::free_balance(2),
				init_balance_2 + total_payout_1 / 3,
				1
			);
				Balances::free_balance(&10),
				init_balance_10 + total_payout_0 / 3 + total_payout_1 / 3,
fn bond_with_duplicate_vote_should_be_ignored_by_election_provider() {
	ExtBuilder::default()
		.validator_count(2)
		.nominate(false)
		.minimum_validator_count(1)
		.set_stake(31, 1000)
		.build_and_execute(|| {
			assert_eq!(
				<Validators<Test>>::iter()
					.map(|(v, _)| (v, Staking::ledger(v - 1).unwrap().total))
					.collect::<Vec<_>>(),
				vec![(31, 1000), (21, 1000), (11, 1000)],
			);
			assert!(<Nominators<Test>>::iter().map(|(n, _)| n).collect::<Vec<_>>().is_empty());
			let initial_balance = 1000;
				let _ = Balances::make_free_balance_be(i, initial_balance);
			}

			assert_ok!(Staking::bond(
				RuntimeOrigin::signed(1),
				2,
				1000,
				RewardDestination::Controller
			));
			assert_ok!(Staking::nominate(RuntimeOrigin::signed(2), vec![11, 11, 11, 21, 31]));
			assert_ok!(Staking::bond(
				RuntimeOrigin::signed(3),
				4,
				1000,
				RewardDestination::Controller
			));
			assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![21, 31]));
			// winners should be 21 and 31. Otherwise this election is taking duplicates into
			// account.
			let supports = <Test as Config>::ElectionProvider::elect().unwrap();
					(21, Support { total: 1800, voters: vec![(21, 1000), (1, 400), (3, 400)] }),
					(31, Support { total: 2200, voters: vec![(31, 1000), (1, 600), (3, 600)] })
fn bond_with_duplicate_vote_should_be_ignored_by_election_provider_elected() {
	// same as above but ensures that even when the dupe is being elected, everything is sane.
	ExtBuilder::default()
		.validator_count(2)
		.nominate(false)
		.set_stake(31, 1000)
		.minimum_validator_count(1)
		.build_and_execute(|| {
			assert_eq!(
				<Validators<Test>>::iter()
					.map(|(v, _)| (v, Staking::ledger(v - 1).unwrap().total))
					.collect::<Vec<_>>(),
				vec![(31, 1000), (21, 1000), (11, 1000)],
			assert!(<Nominators<Test>>::iter().collect::<Vec<_>>().is_empty());
			let initial_balance = 1000;
			for i in [1, 2, 3, 4].iter() {
				let _ = Balances::make_free_balance_be(i, initial_balance);
			}

			assert_ok!(Staking::bond(
				RuntimeOrigin::signed(1),
				2,
				1000,
				RewardDestination::Controller
			));
			assert_ok!(Staking::nominate(RuntimeOrigin::signed(2), vec![11, 11, 11, 21]));
			assert_ok!(Staking::bond(
				RuntimeOrigin::signed(3),
				4,
				1000,
				RewardDestination::Controller
			));
			assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![21]));
			let supports = <Test as Config>::ElectionProvider::elect().unwrap();
			assert_eq!(
				supports,
				vec![
					(11, Support { total: 1500, voters: vec![(11, 1000), (1, 500)] }),
					(21, Support { total: 2500, voters: vec![(21, 1000), (1, 500), (3, 1000)] })
fn new_era_elects_correct_number_of_validators() {
	ExtBuilder::default().nominate(true).validator_count(1).build_and_execute(|| {
		assert_eq!(Staking::validator_count(), 1);
		assert_eq!(validator_controllers().len(), 1);
		Session::on_initialize(System::block_number());
		assert_eq!(validator_controllers().len(), 1);
	})
fn phragmen_should_not_overflow() {
	ExtBuilder::default().nominate(false).build_and_execute(|| {
		// This is the maximum value that we can have as the outcome of CurrencyToVote.
		type Votes = u64;
		let _ = Staking::chill(RuntimeOrigin::signed(10));
		let _ = Staking::chill(RuntimeOrigin::signed(20));
		bond_validator(3, 2, Votes::max_value() as Balance);
		bond_validator(5, 4, Votes::max_value() as Balance);
		bond_nominator(7, 6, Votes::max_value() as Balance, vec![3, 5]);
		bond_nominator(9, 8, Votes::max_value() as Balance, vec![3, 5]);
		assert_eq_uvec!(validator_controllers(), vec![4, 2]);
		// We can safely convert back to values within [u64, u128].
		assert!(Staking::eras_stakers(active_era(), 3).total > Votes::max_value() as Balance);
		assert!(Staking::eras_stakers(active_era(), 5).total > Votes::max_value() as Balance);
fn reward_validator_slashing_validator_does_not_overflow() {
	ExtBuilder::default().build_and_execute(|| {
		let stake = u64::MAX as Balance * 2;
		let reward_slash = u64::MAX as Balance * 2;

		// Assert multiplication overflows in balance arithmetic.
		assert!(stake.checked_mul(reward_slash).is_none());

		// Set staker
		let _ = Balances::make_free_balance_be(&11, stake);
Gavin Wood's avatar
Gavin Wood committed

		let exposure = Exposure::<AccountId, Balance> { total: stake, own: stake, others: vec![] };
		let reward = EraRewardPoints::<AccountId> {
			total: 1,
			individual: vec![(11, 1)].into_iter().collect(),
		};
Gavin Wood's avatar
Gavin Wood committed
		ErasRewardPoints::<Test>::insert(0, reward);
		ErasStakers::<Test>::insert(0, 11, &exposure);
		ErasStakersClipped::<Test>::insert(0, 11, exposure);
		ErasValidatorReward::<Test>::insert(0, stake);
		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 0));
		assert_eq!(Balances::total_balance(&11), stake * 2);

		// Set staker
		let _ = Balances::make_free_balance_be(&11, stake);
		let _ = Balances::make_free_balance_be(&2, stake);
		// only slashes out of bonded stake are applied. without this line, it is 0.
		Staking::bond(RuntimeOrigin::signed(2), 20000, stake - 1, RewardDestination::default())
			.unwrap();
Gavin Wood's avatar
Gavin Wood committed
		// Override exposure of 11
		ErasStakers::<Test>::insert(
			0,
			11,
			Exposure {
				total: stake,
				own: 1,
				others: vec![IndividualExposure { who: 2, value: stake - 1 }],
			},
		);
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(100)],
		);

		assert_eq!(Balances::total_balance(&11), stake - 1);
		assert_eq!(Balances::total_balance(&2), 1);
	})
}

#[test]
fn reward_from_authorship_event_handler_works() {
	ExtBuilder::default().build_and_execute(|| {
		use pallet_authorship::EventHandler;
		assert_eq!(<pallet_authorship::Pallet<Test>>::author(), Some(11));
		Pallet::<Test>::note_author(11);
		Pallet::<Test>::note_author(11);

		// Not mandatory but must be coherent with rewards
Gavin Wood's avatar
Gavin Wood committed
		assert_eq_uvec!(Session::validators(), vec![11, 21]);
thiolliere's avatar
thiolliere committed
		// 21 is rewarded as an uncle producer
		// 11 is rewarded as a block producer and uncle referencer and uncle producer
Gavin Wood's avatar
Gavin Wood committed
		assert_eq!(
			ErasRewardPoints::<Test>::get(active_era()),
			EraRewardPoints { individual: vec![(11, 20 * 2)].into_iter().collect(), total: 40 },
Gavin Wood's avatar
Gavin Wood committed
		);
thiolliere's avatar
thiolliere committed
	})
}

#[test]
fn add_reward_points_fns_works() {
	ExtBuilder::default().build_and_execute(|| {
thiolliere's avatar
thiolliere committed
		// Not mandatory but must be coherent with rewards
		assert_eq_uvec!(Session::validators(), vec![21, 11]);
		Pallet::<Test>::reward_by_ids(vec![(21, 1), (11, 1), (11, 1)]);
		Pallet::<Test>::reward_by_ids(vec![(21, 1), (11, 1), (11, 1)]);
Gavin Wood's avatar
Gavin Wood committed
		assert_eq!(
			ErasRewardPoints::<Test>::get(active_era()),
			EraRewardPoints { individual: vec![(11, 4), (21, 2)].into_iter().collect(), total: 6 },
Gavin Wood's avatar
Gavin Wood committed
		);
Gavin Wood's avatar
Gavin Wood committed

#[test]
fn unbonded_balance_is_not_slashable() {
	ExtBuilder::default().build_and_execute(|| {
Gavin Wood's avatar
Gavin Wood committed
		// total amount staked is slashable.
		assert_eq!(Staking::slashable_balance_of(&11), 1000);

		assert_ok!(Staking::unbond(RuntimeOrigin::signed(10), 800));
Gavin Wood's avatar
Gavin Wood committed

		// only the active portion.
		assert_eq!(Staking::slashable_balance_of(&11), 200);
	})
}

#[test]
fn era_is_always_same_length() {
	// This ensures that the sessions is always of the same length if there is no forcing no
	// session changes.
	ExtBuilder::default().build_and_execute(|| {
		let session_per_era = <SessionsPerEra as Get<SessionIndex>>::get();

		assert_eq!(Staking::eras_start_session_index(current_era()).unwrap(), session_per_era);
		assert_eq!(
			Staking::eras_start_session_index(current_era()).unwrap(),
			session_per_era * 2u32
		);

		let session = Session::current_index();
		Staking::set_force_era(Forcing::ForceNew);
		advance_session();
Gavin Wood's avatar
Gavin Wood committed
		advance_session();
		assert_eq!(current_era(), 3);
		assert_eq!(Staking::eras_start_session_index(current_era()).unwrap(), session + 2);
		assert_eq!(
			Staking::eras_start_session_index(current_era()).unwrap(),
			session + 2u32 + session_per_era
		);
	});
}

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

		assert_eq!(Staking::force_era(), Forcing::ForceNew);
	});
}

#[test]
fn offence_ensures_new_era_without_clobbering() {
	ExtBuilder::default().build_and_execute(|| {
		assert_ok!(Staking::force_new_era_always(RuntimeOrigin::root()));
		assert_eq!(Staking::force_era(), Forcing::ForceAlways);
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![],
			}],
			&[Perbill::from_percent(5)],
		);

		assert_eq!(Staking::force_era(), Forcing::ForceAlways);
	});
}

#[test]
fn offence_deselects_validator_even_when_slash_is_zero() {
	ExtBuilder::default().build_and_execute(|| {
Gavin Wood's avatar
Gavin Wood committed
		assert!(Session::validators().contains(&11));
		assert!(<Validators<Test>>::contains_key(11));
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![],
			}],
			&[Perbill::from_percent(0)],
		);
		assert_eq!(Staking::force_era(), Forcing::ForceNew);
		assert!(!<Validators<Test>>::contains_key(11));
Gavin Wood's avatar
Gavin Wood committed
		assert!(!Session::validators().contains(&11));
		assert!(!<Validators<Test>>::contains_key(11));
#[test]
fn slashing_performed_according_exposure() {
	// This test checks that slashing is performed according the exposure (or more precisely,
	// historical exposure), not the current balance.
	ExtBuilder::default().build_and_execute(|| {
		assert_eq!(Staking::eras_stakers(active_era(), 11).own, 1000);

		// Handle an offence with a historical exposure.
			&[OffenceDetails {
				offender: (11, Exposure { total: 500, own: 500, others: vec![] }),
				reporters: vec![],
			}],
			&[Perbill::from_percent(50)],
		);

		// The stash account should be slashed for 250 (50% of 500).
		assert_eq!(Balances::free_balance(11), 1000 - 250);
#[test]
fn slash_in_old_span_does_not_deselect() {
	ExtBuilder::default().build_and_execute(|| {
		assert!(<Validators<Test>>::contains_key(11));
Gavin Wood's avatar
Gavin Wood committed
		assert!(Session::validators().contains(&11));
		on_offence_now(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![],
			}],
			&[Perbill::from_percent(0)],
		);
		assert_eq!(Staking::force_era(), Forcing::ForceNew);
		assert!(!<Validators<Test>>::contains_key(11));
		Staking::validate(RuntimeOrigin::signed(10), Default::default()).unwrap();
		assert_eq!(Staking::force_era(), Forcing::NotForcing);
		assert!(<Validators<Test>>::contains_key(11));
Gavin Wood's avatar
Gavin Wood committed
		assert!(!Session::validators().contains(&11));

		// this staker is in a new slashing span now, having re-registered after
		// their prior slash.

		on_offence_in_era(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![],
			}],
			&[Perbill::from_percent(0)],
			1,
		assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));

		// but we are still forcing a new era
		assert_eq!(Staking::force_era(), Forcing::ForceNew);

		on_offence_in_era(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![],
			}],
			// NOTE: A 100% slash here would clean up the account, causing de-registration.
			&[Perbill::from_percent(95)],
		assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));

		// but it's disabled
		assert!(is_disabled(10));
		// and we are still forcing a new era
		assert_eq!(Staking::force_era(), Forcing::ForceNew);
#[test]
fn reporters_receive_their_slice() {
	// This test verifies that the reporters of the offence receive their slice from the slashed
	// amount.
	ExtBuilder::default().build_and_execute(|| {
		// The reporters' reward is calculated from the total exposure.
Kian Paimani's avatar
Kian Paimani committed
		let initial_balance = 1125;

		assert_eq!(Staking::eras_stakers(active_era(), 11).total, initial_balance);
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![1, 2],
			}],
			&[Perbill::from_percent(50)],
		);

		// F1 * (reward_proportion * slash - 0)
		// 50% * (10% * initial_balance / 2)
		let reward = (initial_balance / 20) / 2;
		let reward_each = reward / 2; // split into two pieces.
		assert_eq!(Balances::free_balance(1), 10 + reward_each);
		assert_eq!(Balances::free_balance(2), 20 + reward_each);
fn subsequent_reports_in_same_span_pay_out_less() {
	// This test verifies that the reporters of the offence receive their slice from the slashed
	// amount, but less and less if they submit multiple reports in one span.
	ExtBuilder::default().build_and_execute(|| {
		// The reporters' reward is calculated from the total exposure.
		let initial_balance = 1125;

		assert_eq!(Staking::eras_stakers(active_era(), 11).total, initial_balance);
		on_offence_now(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![1],
			}],
			&[Perbill::from_percent(20)],
		);

		// F1 * (reward_proportion * slash - 0)
		// 50% * (10% * initial_balance * 20%)
		let reward = (initial_balance / 5) / 20;
		assert_eq!(Balances::free_balance(1), 10 + reward);

		on_offence_now(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![1],
			}],
			&[Perbill::from_percent(50)],
		);

		let prior_payout = reward;

		// F1 * (reward_proportion * slash - prior_payout)
		// 50% * (10% * (initial_balance / 2) - prior_payout)
		let reward = ((initial_balance / 20) - prior_payout) / 2;
		assert_eq!(Balances::free_balance(1), 10 + prior_payout + reward);
	});
}

#[test]
fn invulnerables_are_not_slashed() {
	// For invulnerable validators no slashing is performed.
	ExtBuilder::default().invulnerables(vec![11]).build_and_execute(|| {
		assert_eq!(Balances::free_balance(11), 1000);
		assert_eq!(Balances::free_balance(21), 2000);
		let exposure = Staking::eras_stakers(active_era(), 21);
		let initial_balance = Staking::slashable_balance_of(&21);

		let nominator_balances: Vec<_> =
			exposure.others.iter().map(|o| Balances::free_balance(&o.who)).collect();
					offender: (11, Staking::eras_stakers(active_era(), 11)),
					reporters: vec![],
				},
				OffenceDetails {
					offender: (21, Staking::eras_stakers(active_era(), 21)),
					reporters: vec![],
				},
			],
			&[Perbill::from_percent(50), Perbill::from_percent(20)],
		);
		// The validator 11 hasn't been slashed, but 21 has been.
		assert_eq!(Balances::free_balance(11), 1000);
		// 2000 - (0.2 * initial_balance)
		assert_eq!(Balances::free_balance(21), 2000 - (2 * initial_balance / 10));

		// ensure that nominators were slashed as well.
		for (initial_balance, other) in nominator_balances.into_iter().zip(exposure.others) {
			assert_eq!(
				Balances::free_balance(&other.who),
				initial_balance - (2 * other.value / 10),
			);
		}
}

#[test]
fn dont_slash_if_fraction_is_zero() {
	// Don't slash if the fraction is zero.
	ExtBuilder::default().build_and_execute(|| {
		assert_eq!(Balances::free_balance(11), 1000);
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
				reporters: vec![],
			}],
			&[Perbill::from_percent(0)],
		);

		// The validator hasn't been slashed. The new era is not forced.
		assert_eq!(Balances::free_balance(11), 1000);
		assert_eq!(Staking::force_era(), Forcing::ForceNew);
	});
}

#[test]
fn only_slash_for_max_in_era() {
	// multiple slashes within one era are only applied if it is more than any previous slash in the
	// same era.
	ExtBuilder::default().build_and_execute(|| {
		assert_eq!(Balances::free_balance(11), 1000);
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(50)],
		);

		// The validator has been slashed and has been force-chilled.
		assert_eq!(Balances::free_balance(11), 500);
		assert_eq!(Staking::force_era(), Forcing::ForceNew);

		on_offence_now(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(25)],
		);

		// The validator has not been slashed additionally.
		assert_eq!(Balances::free_balance(11), 500);
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(60)],
		);

		// The validator got slashed 10% more.
		assert_eq!(Balances::free_balance(11), 400);
	})
}

#[test]
fn garbage_collection_after_slashing() {
	// ensures that `SlashingSpans` and `SpanSlash` of an account is removed after reaping.
	ExtBuilder::default()
		.existential_deposit(2)
		.balance_factor(2)
		.build_and_execute(|| {
			assert_eq!(Balances::free_balance(11), 2000);
			on_offence_now(
				&[OffenceDetails {
					offender: (11, Staking::eras_stakers(active_era(), 11)),
					reporters: vec![],
				}],
				&[Perbill::from_percent(10)],
			);
			assert_eq!(Balances::free_balance(11), 2000 - 200);
			assert!(SlashingSpans::<Test>::get(&11).is_some());
			assert_eq!(SpanSlash::<Test>::get(&(11, 0)).amount(), &200);
			on_offence_now(
				&[OffenceDetails {
					offender: (11, Staking::eras_stakers(active_era(), 11)),
					reporters: vec![],
				}],
				&[Perbill::from_percent(100)],
			);
			// validator and nominator slash in era are garbage-collected by era change,
			// so we don't test those here.
			assert_eq!(Balances::free_balance(11), 2);
			assert_eq!(Balances::total_balance(&11), 2);
Gavin Wood's avatar
Gavin Wood committed

			let slashing_spans = SlashingSpans::<Test>::get(&11).unwrap();
			assert_eq!(slashing_spans.iter().count(), 2);
			// reap_stash respects num_slashing_spans so that weight is accurate
			assert_noop!(
				Staking::reap_stash(RuntimeOrigin::signed(20), 11, 0),
				Error::<Test>::IncorrectSlashingSpans
			);
			assert_ok!(Staking::reap_stash(RuntimeOrigin::signed(20), 11, 2));
Gavin Wood's avatar
Gavin Wood committed

			assert!(SlashingSpans::<Test>::get(&11).is_none());
			assert_eq!(SpanSlash::<Test>::get(&(11, 0)).amount(), &0);
}

#[test]
fn garbage_collection_on_window_pruning() {
	// ensures that `ValidatorSlashInEra` and `NominatorSlashInEra` are cleared after
	// `BondingDuration`.
	ExtBuilder::default().build_and_execute(|| {
		assert_eq!(Balances::free_balance(11), 1000);
		let now = active_era();
		let exposure = Staking::eras_stakers(now, 11);
		assert_eq!(Balances::free_balance(101), 2000);
		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;

		on_offence_now(
			&[OffenceDetails { offender: (11, Staking::eras_stakers(now, 11)), reporters: vec![] }],
			&[Perbill::from_percent(10)],
		);

		assert_eq!(Balances::free_balance(11), 900);
		assert_eq!(Balances::free_balance(101), 2000 - (nominated_value / 10));
		assert!(ValidatorSlashInEra::<Test>::get(&now, &11).is_some());
		assert!(NominatorSlashInEra::<Test>::get(&now, &101).is_some());

		// + 1 because we have to exit the bonding window.
		for era in (0..(BondingDuration::get() + 1)).map(|offset| offset + now + 1) {
			assert!(ValidatorSlashInEra::<Test>::get(&now, &11).is_some());
			assert!(NominatorSlashInEra::<Test>::get(&now, &101).is_some());
		assert!(ValidatorSlashInEra::<Test>::get(&now, &11).is_none());
		assert!(NominatorSlashInEra::<Test>::get(&now, &101).is_none());
	})
}

#[test]
fn slashing_nominators_by_span_max() {
	ExtBuilder::default().build_and_execute(|| {
		mock::start_active_era(1);
		mock::start_active_era(2);
		mock::start_active_era(3);
		assert_eq!(Balances::free_balance(11), 1000);
		assert_eq!(Balances::free_balance(21), 2000);
		assert_eq!(Balances::free_balance(101), 2000);
		assert_eq!(Staking::slashable_balance_of(&21), 1000);

		let exposure_11 = Staking::eras_stakers(active_era(), 11);
		let exposure_21 = Staking::eras_stakers(active_era(), 21);
		let nominated_value_11 = exposure_11.others.iter().find(|o| o.who == 101).unwrap().value;
		let nominated_value_21 = exposure_21.others.iter().find(|o| o.who == 101).unwrap().value;

		on_offence_in_era(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(10)],
			2,
		assert_eq!(Balances::free_balance(11), 900);

		let slash_1_amount = Perbill::from_percent(10) * nominated_value_11;
		assert_eq!(Balances::free_balance(101), 2000 - slash_1_amount);

		let expected_spans = vec![
			slashing::SlashingSpan { index: 1, start: 4, length: None },
			slashing::SlashingSpan { index: 0, start: 0, length: Some(4) },
		];

		let get_span = |account| SlashingSpans::<Test>::get(&account).unwrap();
		assert_eq!(get_span(11).iter().collect::<Vec<_>>(), expected_spans);
		assert_eq!(get_span(101).iter().collect::<Vec<_>>(), expected_spans);

		// second slash: higher era, higher value, same span.
		on_offence_in_era(
			&[OffenceDetails {
				offender: (21, Staking::eras_stakers(active_era(), 21)),
			&[Perbill::from_percent(30)],
			3,
		);

		// 11 was not further slashed, but 21 and 101 were.
		assert_eq!(Balances::free_balance(11), 900);
		assert_eq!(Balances::free_balance(21), 1700);

		let slash_2_amount = Perbill::from_percent(30) * nominated_value_21;
		assert!(slash_2_amount > slash_1_amount);

		// only the maximum slash in a single span is taken.
		assert_eq!(Balances::free_balance(101), 2000 - slash_2_amount);

		// third slash: in same era and on same validator as first, higher
		// in-era value, but lower slash value than slash 2.
		on_offence_in_era(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(20)],
			2,
		);

		// 11 was further slashed, but 21 and 101 were not.
		assert_eq!(Balances::free_balance(11), 800);
		assert_eq!(Balances::free_balance(21), 1700);

		let slash_3_amount = Perbill::from_percent(20) * nominated_value_21;
		assert!(slash_3_amount < slash_2_amount);
		assert!(slash_3_amount > slash_1_amount);

		// only the maximum slash in a single span is taken.
		assert_eq!(Balances::free_balance(101), 2000 - slash_2_amount);
	});
}

#[test]
fn slashes_are_summed_across_spans() {
	ExtBuilder::default().build_and_execute(|| {
		mock::start_active_era(1);
		mock::start_active_era(2);
		mock::start_active_era(3);
		assert_eq!(Balances::free_balance(21), 2000);
		assert_eq!(Staking::slashable_balance_of(&21), 1000);

		let get_span = |account| SlashingSpans::<Test>::get(&account).unwrap();
			&[OffenceDetails {
				offender: (21, Staking::eras_stakers(active_era(), 21)),
			&[Perbill::from_percent(10)],
		);

		let expected_spans = vec![
			slashing::SlashingSpan { index: 1, start: 4, length: None },
			slashing::SlashingSpan { index: 0, start: 0, length: Some(4) },
		];

		assert_eq!(get_span(21).iter().collect::<Vec<_>>(), expected_spans);
		assert_eq!(Balances::free_balance(21), 1900);

		// 21 has been force-chilled. re-signal intent to validate.
		Staking::validate(RuntimeOrigin::signed(20), Default::default()).unwrap();

		assert_eq!(Staking::slashable_balance_of(&21), 900);

		on_offence_now(
			&[OffenceDetails {
				offender: (21, Staking::eras_stakers(active_era(), 21)),
			&[Perbill::from_percent(10)],
		);

		let expected_spans = vec![
			slashing::SlashingSpan { index: 2, start: 5, length: None },
			slashing::SlashingSpan { index: 1, start: 4, length: Some(1) },
			slashing::SlashingSpan { index: 0, start: 0, length: Some(4) },
		];

		assert_eq!(get_span(21).iter().collect::<Vec<_>>(), expected_spans);
		assert_eq!(Balances::free_balance(21), 1810);
	});
}

#[test]
fn deferred_slashes_are_deferred() {
	ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| {
		mock::start_active_era(1);
		assert_eq!(Balances::free_balance(11), 1000);
		let exposure = Staking::eras_stakers(active_era(), 11);
		assert_eq!(Balances::free_balance(101), 2000);
		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
		on_offence_now(
			&[OffenceDetails {
				offender: (11, Staking::eras_stakers(active_era(), 11)),
			&[Perbill::from_percent(10)],
		);

		// nominations are not removed regardless of the deferring.
		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);

		assert_eq!(Balances::free_balance(11), 1000);
		assert_eq!(Balances::free_balance(101), 2000);
		mock::start_active_era(2);
		assert_eq!(Balances::free_balance(11), 1000);
		assert_eq!(Balances::free_balance(101), 2000);
		mock::start_active_era(3);
		assert_eq!(Balances::free_balance(11), 1000);
		assert_eq!(Balances::free_balance(101), 2000);
		// at the start of era 4, slashes from era 1 are processed,
		// after being deferred for at least 2 full eras.
		mock::start_active_era(4);
		assert_eq!(Balances::free_balance(11), 900);
		assert_eq!(Balances::free_balance(101), 2000 - (nominated_value / 10));
		assert!(matches!(
			staking_events_since_last_call().as_slice(),
			&[
				Event::Chilled { stash: 11 },
				Event::ForceEra { mode: Forcing::ForceNew },
				Event::SlashReported { validator: 11, slash_era: 1, .. },
				Event::StakersElected,
				Event::ForceEra { mode: Forcing::NotForcing },
				Event::Slashed { staker: 11, amount: 100 },
				Event::Slashed { staker: 101, amount: 12 }
	})
}

#[test]
fn retroactive_deferred_slashes_two_eras_before() {
	ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| {
		assert_eq!(BondingDuration::get(), 3);

		mock::start_active_era(1);
		let exposure_11_at_era1 = Staking::eras_stakers(active_era(), 11);

		mock::start_active_era(3);

		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);

		System::reset_events();
		on_offence_in_era(
			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
			&[Perbill::from_percent(10)],
			1, // should be deferred for two full eras, and applied at the beginning of era 4.
			DisableStrategy::Never,
		);

		mock::start_active_era(4);

		assert!(matches!(
			staking_events_since_last_call().as_slice(),
			&[
				Event::Chilled { stash: 11 },
				Event::ForceEra { mode: Forcing::ForceNew },
				Event::SlashReported { validator: 11, slash_era: 1, .. },
				..,
				Event::Slashed { staker: 11, amount: 100 },
				Event::Slashed { staker: 101, amount: 12 }
	})
}

#[test]
fn retroactive_deferred_slashes_one_before() {
	ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| {
		assert_eq!(BondingDuration::get(), 3);

		mock::start_active_era(1);
		let exposure_11_at_era1 = Staking::eras_stakers(active_era(), 11);

		// unbond at slash era.
		mock::start_active_era(2);
		assert_ok!(Staking::chill(RuntimeOrigin::signed(10)));
		assert_ok!(Staking::unbond(RuntimeOrigin::signed(10), 100));

		mock::start_active_era(3);
		System::reset_events();
		on_offence_in_era(
			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
			&[Perbill::from_percent(10)],
			2, // should be deferred for two full eras, and applied at the beginning of era 5.
			DisableStrategy::Never,
		);

		mock::start_active_era(4);

		assert_eq!(Staking::ledger(10).unwrap().total, 1000);
		// slash happens after the next line.
		mock::start_active_era(5);
		assert!(matches!(
			staking_events_since_last_call().as_slice(),
			&[
				Event::SlashReported { validator: 11, slash_era: 2, .. },
				..,
				Event::Slashed { staker: 11, amount: 100 },
				Event::Slashed { staker: 101, amount: 12 }

		// their ledger has already been slashed.
		assert_eq!(Staking::ledger(10).unwrap().total, 900);
		assert_ok!(Staking::unbond(RuntimeOrigin::signed(10), 1000));