From 065e64d1e414ca4478c9d3665889994c04705c10 Mon Sep 17 00:00:00 2001
From: Tsvetomir Dimitrov <tsvetomir@parity.io>
Date: Sun, 16 Mar 2025 10:36:02 +0200
Subject: [PATCH] Revert "[Staking] Bounded Slashing: Paginated Offence
 Processing & Slash Application (#7424)"

This reverts commit dda2cb5969985ccbf67581e18eb7c579849e27bb.
---
 polkadot/runtime/test-runtime/src/lib.rs      |   4 +-
 polkadot/runtime/westend/src/lib.rs           |   1 -
 .../westend/src/weights/pallet_staking.rs     |  28 -
 prdoc/pr_7424.prdoc                           |  37 -
 substrate/bin/node/runtime/src/lib.rs         |  54 +-
 substrate/frame/babe/src/mock.rs              |   4 +-
 substrate/frame/beefy/src/mock.rs             |   4 +-
 .../test-staking-e2e/src/mock.rs              |   9 +-
 substrate/frame/grandpa/src/mock.rs           |   4 +-
 .../frame/offences/benchmarking/src/inner.rs  |  15 +-
 .../frame/offences/benchmarking/src/mock.rs   |   5 +-
 substrate/frame/root-offences/src/lib.rs      |  17 +-
 substrate/frame/root-offences/src/mock.rs     |  11 +-
 substrate/frame/root-offences/src/tests.rs    |  12 +-
 .../frame/session/benchmarking/src/mock.rs    |   6 +-
 substrate/frame/staking/src/benchmarking.rs   |  70 +-
 substrate/frame/staking/src/lib.rs            |  42 +-
 substrate/frame/staking/src/migrations.rs     | 340 ++++++---
 substrate/frame/staking/src/mock.rs           |  42 +-
 substrate/frame/staking/src/pallet/impls.rs   | 259 +++----
 substrate/frame/staking/src/pallet/mod.rs     | 195 +-----
 substrate/frame/staking/src/slashing.rs       | 400 ++++-------
 substrate/frame/staking/src/tests.rs          | 651 ++++++++++++------
 substrate/frame/staking/src/weights.rs        |  56 +-
 24 files changed, 1102 insertions(+), 1164 deletions(-)
 delete mode 100644 prdoc/pr_7424.prdoc

diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs
index 226e22c0783..694077dd21c 100644
--- a/polkadot/runtime/test-runtime/src/lib.rs
+++ b/polkadot/runtime/test-runtime/src/lib.rs
@@ -323,8 +323,8 @@ impl pallet_session::Config for Runtime {
 }
 
 impl pallet_session::historical::Config for Runtime {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
 }
 
 pallet_staking_reward_curve::build! {
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index b5dc9b8f55c..86358afb23e 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -1874,7 +1874,6 @@ pub mod migrations {
 		parachains_shared::migration::MigrateToV1<Runtime>,
 		parachains_scheduler::migration::MigrateV2ToV3<Runtime>,
 		pallet_staking::migrations::v16::MigrateV15ToV16<Runtime>,
-		pallet_staking::migrations::v17::MigrateV16ToV17<Runtime>,
 		pallet_session::migrations::v1::MigrateV0ToV1<
 			Runtime,
 			pallet_staking::migrations::v17::MigrateDisabledToSession<Runtime>,
diff --git a/polkadot/runtime/westend/src/weights/pallet_staking.rs b/polkadot/runtime/westend/src/weights/pallet_staking.rs
index 496bc01e5e3..b92d54f3b94 100644
--- a/polkadot/runtime/westend/src/weights/pallet_staking.rs
+++ b/polkadot/runtime/westend/src/weights/pallet_staking.rs
@@ -847,34 +847,6 @@ impl<T: frame_system::Config> pallet_staking::WeightInfo for WeightInfo<T> {
 			.saturating_add(T::DbWeight::get().reads(6))
 			.saturating_add(T::DbWeight::get().writes(2))
 	}
-	/// Storage: `Staking::ActiveEra` (r:1 w:0)
-	/// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::UnappliedSlashes` (r:1 w:1)
-	/// Proof: `Staking::UnappliedSlashes` (`max_values`: None, `max_size`: Some(3231), added: 5706, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::Bonded` (r:65 w:0)
-	/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::Ledger` (r:65 w:65)
-	/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
-	/// Storage: `NominationPools::ReversePoolIdLookup` (r:65 w:0)
-	/// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`)
-	/// Storage: `DelegatedStaking::Agents` (r:65 w:65)
-	/// Proof: `DelegatedStaking::Agents` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`)
-	/// Storage: `System::Account` (r:65 w:65)
-	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::VirtualStakers` (r:65 w:0)
-	/// Proof: `Staking::VirtualStakers` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`)
-	/// Storage: `Balances::Holds` (r:65 w:65)
-	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`)
-	fn apply_slash() -> Weight {
-		// Proof Size summary in bytes:
-		//  Measured:  `29228`
-		//  Estimated: `232780`
-		// Minimum execution time: 3_571_461_000 picoseconds.
-		Weight::from_parts(3_638_696_000, 0)
-			.saturating_add(Weight::from_parts(0, 232780))
-			.saturating_add(T::DbWeight::get().reads(457))
-			.saturating_add(T::DbWeight::get().writes(261))
-	}
 	/// Storage: `Staking::CurrentEra` (r:1 w:0)
 	/// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
 	/// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0)
diff --git a/prdoc/pr_7424.prdoc b/prdoc/pr_7424.prdoc
deleted file mode 100644
index e177f41371b..00000000000
--- a/prdoc/pr_7424.prdoc
+++ /dev/null
@@ -1,37 +0,0 @@
-# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
-# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
-
-title: 'Bounded Slashing: Paginated Offence Processing & Slash Application'
-
-doc:
-  - audience: Runtime Dev
-    description: |
-      This PR refactors the slashing mechanism in `pallet-staking` to be bounded by introducing paged offence processing and paged slash application.
-
-            ### Key Changes
-            - Offences are queued instead of being processed immediately.
-            - Slashes are computed in pages, stored as a `StorageDoubleMap` with `(Validator, SlashFraction, PageIndex)` to uniquely identify them.
-            - Slashes are applied incrementally across multiple blocks instead of a single unbounded operation.
-            - New storage items: `OffenceQueue`, `ProcessingOffence`, `OffenceQueueEras`.
-            - Updated API for cancelling and applying slashes.
-            - Preliminary benchmarks added; further optimizations planned.
-
-            This enables staking slashing to scale efficiently and removes a major blocker for staking migration to a parachain (AH).
-
-crates:
-- name: pallet-babe
-  bump: patch
-- name: pallet-staking
-  bump: major
-- name: pallet-grandpa
-  bump: patch
-- name: westend-runtime
-  bump: minor
-- name: pallet-beefy
-  bump: patch
-- name: pallet-offences-benchmarking
-  bump: patch
-- name: pallet-session-benchmarking
-  bump: patch
-- name: pallet-root-offences
-  bump: patch
\ No newline at end of file
diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs
index 94729a26b6d..c618831c0a7 100644
--- a/substrate/bin/node/runtime/src/lib.rs
+++ b/substrate/bin/node/runtime/src/lib.rs
@@ -680,6 +680,8 @@ impl_opaque_keys! {
 
 #[cfg(feature = "staking-playground")]
 pub mod staking_playground {
+	use pallet_staking::Exposure;
+
 	use super::*;
 
 	/// An adapter to make the chain work with --dev only, even though it is running a large staking
@@ -714,43 +716,61 @@ pub mod staking_playground {
 		}
 	}
 
-	impl pallet_session::historical::SessionManager<AccountId, ()> for AliceAsOnlyValidator {
+	impl pallet_session::historical::SessionManager<AccountId, Exposure<AccountId, Balance>>
+		for AliceAsOnlyValidator
+	{
 		fn end_session(end_index: sp_staking::SessionIndex) {
-			<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::end_session(
-				end_index,
-			)
+			<Staking as pallet_session::historical::SessionManager<
+				AccountId,
+				Exposure<AccountId, Balance>,
+			>>::end_session(end_index)
 		}
 
-		fn new_session(new_index: sp_staking::SessionIndex) -> Option<Vec<(AccountId, ())>> {
-			<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::new_session(
-				new_index,
-			)
+		fn new_session(
+			new_index: sp_staking::SessionIndex,
+		) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> {
+			<Staking as pallet_session::historical::SessionManager<
+				AccountId,
+				Exposure<AccountId, Balance>,
+			>>::new_session(new_index)
 			.map(|_ignored| {
 				// construct a fake exposure for alice.
-				vec![(sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(), ())]
+				vec![(
+					sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(),
+					pallet_staking::Exposure {
+						total: 1_000_000_000,
+						own: 1_000_000_000,
+						others: vec![],
+					},
+				)]
 			})
 		}
 
 		fn new_session_genesis(
 			new_index: sp_staking::SessionIndex,
-		) -> Option<Vec<(AccountId, ())>> {
+		) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> {
 			<Staking as pallet_session::historical::SessionManager<
 				AccountId,
-				(),
+				Exposure<AccountId, Balance>,
 			>>::new_session_genesis(new_index)
 			.map(|_ignored| {
 				// construct a fake exposure for alice.
 				vec![(
 					sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(),
-					(),
+					pallet_staking::Exposure {
+						total: 1_000_000_000,
+						own: 1_000_000_000,
+						others: vec![],
+					},
 				)]
 			})
 		}
 
 		fn start_session(start_index: sp_staking::SessionIndex) {
-			<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::start_session(
-				start_index,
-			)
+			<Staking as pallet_session::historical::SessionManager<
+				AccountId,
+				Exposure<AccountId, Balance>,
+			>>::start_session(start_index)
 		}
 	}
 }
@@ -776,8 +796,8 @@ impl pallet_session::Config for Runtime {
 }
 
 impl pallet_session::historical::Config for Runtime {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
 }
 
 pallet_staking_reward_curve::build! {
diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs
index ea977a547fe..eeaebe02d3e 100644
--- a/substrate/frame/babe/src/mock.rs
+++ b/substrate/frame/babe/src/mock.rs
@@ -105,8 +105,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<u64, u128>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
 }
 
 impl pallet_authorship::Config for Test {
diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs
index 275bf18fe87..46491996623 100644
--- a/substrate/frame/beefy/src/mock.rs
+++ b/substrate/frame/beefy/src/mock.rs
@@ -189,8 +189,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<u64, u128>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
 }
 
 impl pallet_authorship::Config for Test {
diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs
index e4b77975707..135a52fece6 100644
--- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs
+++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs
@@ -147,8 +147,8 @@ impl pallet_session::Config for Runtime {
 	type WeightInfo = ();
 }
 impl pallet_session::historical::Config for Runtime {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
 }
 
 frame_election_provider_support::generate_solution_type!(
@@ -909,7 +909,10 @@ pub(crate) fn on_offence_now(
 // Add offence to validator, slash it.
 pub(crate) fn add_slash(who: &AccountId) {
 	on_offence_now(
-		&[OffenceDetails { offender: (*who, ()), reporters: vec![] }],
+		&[OffenceDetails {
+			offender: (*who, Staking::eras_stakers(active_era(), who)),
+			reporters: vec![],
+		}],
 		&[Perbill::from_percent(10)],
 	);
 }
diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs
index 482e767d32f..2fd0cbb5ffd 100644
--- a/substrate/frame/grandpa/src/mock.rs
+++ b/substrate/frame/grandpa/src/mock.rs
@@ -109,8 +109,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<u64, u128>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
 }
 
 impl pallet_authorship::Config for Test {
diff --git a/substrate/frame/offences/benchmarking/src/inner.rs b/substrate/frame/offences/benchmarking/src/inner.rs
index fa4349d1d94..3d3cd470bc2 100644
--- a/substrate/frame/offences/benchmarking/src/inner.rs
+++ b/substrate/frame/offences/benchmarking/src/inner.rs
@@ -170,13 +170,6 @@ fn make_offenders<T: Config>(
 	Ok(id_tuples)
 }
 
-#[cfg(test)]
-fn run_staking_next_block<T: Config>() {
-	use frame_support::traits::Hooks;
-	System::<T>::set_block_number(System::<T>::block_number().saturating_add(1u32.into()));
-	Staking::<T>::on_initialize(System::<T>::block_number());
-}
-
 #[cfg(test)]
 fn assert_all_slashes_applied<T>(offender_count: usize)
 where
@@ -189,10 +182,10 @@ where
 	// make sure that all slashes have been applied
 	// deposit to reporter + reporter account endowed.
 	assert_eq!(System::<T>::read_events_for_pallet::<pallet_balances::Event<T>>().len(), 2);
-	// (n nominators + one validator) * slashed + Slash Reported + Slash Computed
+	// (n nominators + one validator) * slashed + Slash Reported
 	assert_eq!(
 		System::<T>::read_events_for_pallet::<pallet_staking::Event<T>>().len(),
-		1 * (offender_count + 1) as usize + 2
+		1 * (offender_count + 1) as usize + 1
 	);
 	// offence
 	assert_eq!(System::<T>::read_events_for_pallet::<pallet_offences::Event>().len(), 1);
@@ -239,8 +232,6 @@ mod benchmarks {
 
 		#[cfg(test)]
 		{
-			// slashes applied at the next block.
-			run_staking_next_block::<T>();
 			assert_all_slashes_applied::<T>(n as usize);
 		}
 
@@ -275,8 +266,6 @@ mod benchmarks {
 		}
 		#[cfg(test)]
 		{
-			// slashes applied at the next block.
-			run_staking_next_block::<T>();
 			assert_all_slashes_applied::<T>(n as usize);
 		}
 
diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs
index 63e440d9e00..f37dbf55f52 100644
--- a/substrate/frame/offences/benchmarking/src/mock.rs
+++ b/substrate/frame/offences/benchmarking/src/mock.rs
@@ -33,6 +33,7 @@ use sp_runtime::{
 };
 
 type AccountId = u64;
+type Balance = u64;
 
 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
 impl frame_system::Config for Test {
@@ -53,8 +54,8 @@ impl pallet_timestamp::Config for Test {
 	type WeightInfo = ();
 }
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
 }
 
 sp_runtime::impl_opaque_keys! {
diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs
index 8e91c4ecfd1..fd6ffc55e40 100644
--- a/substrate/frame/root-offences/src/lib.rs
+++ b/substrate/frame/root-offences/src/lib.rs
@@ -31,7 +31,7 @@ extern crate alloc;
 
 use alloc::vec::Vec;
 use pallet_session::historical::IdentificationTuple;
-use pallet_staking::Pallet as Staking;
+use pallet_staking::{BalanceOf, Exposure, ExposureOf, Pallet as Staking};
 use sp_runtime::Perbill;
 use sp_staking::offence::OnOffenceHandler;
 
@@ -49,8 +49,11 @@ pub mod pallet {
 		+ pallet_staking::Config
 		+ pallet_session::Config<ValidatorId = <Self as frame_system::Config>::AccountId>
 		+ pallet_session::historical::Config<
-			FullIdentification = (),
-			FullIdentificationOf = pallet_staking::NullIdentity,
+			FullIdentification = Exposure<
+				<Self as frame_system::Config>::AccountId,
+				BalanceOf<Self>,
+			>,
+			FullIdentificationOf = ExposureOf<Self>,
 		>
 	{
 		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
@@ -103,11 +106,15 @@ pub mod pallet {
 		fn get_offence_details(
 			offenders: Vec<(T::AccountId, Perbill)>,
 		) -> Result<Vec<OffenceDetails<T>>, DispatchError> {
+			let now = pallet_staking::ActiveEra::<T>::get()
+				.map(|e| e.index)
+				.ok_or(Error::<T>::FailedToGetActiveEra)?;
+
 			Ok(offenders
 				.clone()
 				.into_iter()
 				.map(|(o, _)| OffenceDetails::<T> {
-					offender: (o.clone(), ()),
+					offender: (o.clone(), Staking::<T>::eras_stakers(now, &o)),
 					reporters: Default::default(),
 				})
 				.collect())
@@ -117,7 +124,7 @@ pub mod pallet {
 		fn submit_offence(offenders: &[OffenceDetails<T>], slash_fraction: &[Perbill]) {
 			let session_index = <pallet_session::Pallet<T> as frame_support::traits::ValidatorSet<T::AccountId>>::session_index();
 
-			<Staking<T> as OnOffenceHandler<
+			<pallet_staking::Pallet<T> as OnOffenceHandler<
 				T::AccountId,
 				IdentificationTuple<T>,
 				Weight,
diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs
index ce55bdcbdd3..09223802f67 100644
--- a/substrate/frame/root-offences/src/mock.rs
+++ b/substrate/frame/root-offences/src/mock.rs
@@ -28,7 +28,7 @@ use frame_support::{
 	traits::{ConstU32, ConstU64, OneSessionHandler},
 	BoundedVec,
 };
-use pallet_staking::{BalanceOf, StakerStatus};
+use pallet_staking::StakerStatus;
 use sp_core::ConstBool;
 use sp_runtime::{curve::PiecewiseLinear, testing::UintAuthorityId, traits::Zero, BuildStorage};
 use sp_staking::{EraIndex, SessionIndex};
@@ -148,8 +148,8 @@ impl pallet_staking::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = pallet_staking::NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
 }
 
 sp_runtime::impl_opaque_keys! {
@@ -298,11 +298,6 @@ pub(crate) fn run_to_block(n: BlockNumber) {
 	);
 }
 
-/// Progress by n block.
-pub(crate) fn advance_blocks(n: u64) {
-	run_to_block(System::block_number() + n);
-}
-
 pub(crate) fn active_era() -> EraIndex {
 	pallet_staking::ActiveEra::<Test>::get().unwrap().index
 }
diff --git a/substrate/frame/root-offences/src/tests.rs b/substrate/frame/root-offences/src/tests.rs
index da6c49895be..289bb708efb 100644
--- a/substrate/frame/root-offences/src/tests.rs
+++ b/substrate/frame/root-offences/src/tests.rs
@@ -17,10 +17,7 @@
 
 use super::*;
 use frame_support::{assert_err, assert_ok};
-use mock::{
-	active_era, advance_blocks, start_session, ExtBuilder, RootOffences, RuntimeOrigin, System,
-	Test as T,
-};
+use mock::{active_era, start_session, ExtBuilder, RootOffences, RuntimeOrigin, System, Test as T};
 use pallet_staking::asset;
 
 #[test]
@@ -45,10 +42,6 @@ fn create_offence_works_given_root_origin() {
 		assert_ok!(RootOffences::create_offence(RuntimeOrigin::root(), offenders.clone()));
 
 		System::assert_last_event(Event::OffenceCreated { offenders }.into());
-
-		// offence is processed in the following block.
-		advance_blocks(1);
-
 		// the slash should be applied right away.
 		assert_eq!(asset::staked::<T>(&11), 500);
 
@@ -73,9 +66,6 @@ fn create_offence_wont_slash_non_active_validators() {
 
 		System::assert_last_event(Event::OffenceCreated { offenders }.into());
 
-		// advance to the next block so offence gets processed.
-		advance_blocks(1);
-
 		// so 31 didn't get slashed.
 		assert_eq!(asset::staked::<T>(&31), 500);
 
diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs
index 746c3b12e97..235209f14ca 100644
--- a/substrate/frame/session/benchmarking/src/mock.rs
+++ b/substrate/frame/session/benchmarking/src/mock.rs
@@ -27,11 +27,11 @@ use frame_support::{
 	derive_impl, parameter_types,
 	traits::{ConstU32, ConstU64},
 };
-use pallet_staking::NullIdentity;
 use sp_runtime::{traits::IdentityLookup, BuildStorage, KeyTypeId};
 
 type AccountId = u64;
 type Nonce = u32;
+type Balance = u64;
 
 type Block = frame_system::mocking::MockBlock<Test>;
 
@@ -68,8 +68,8 @@ impl pallet_timestamp::Config for Test {
 	type WeightInfo = ();
 }
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = NullIdentity;
+	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
 }
 
 sp_runtime::impl_opaque_keys! {
diff --git a/substrate/frame/staking/src/benchmarking.rs b/substrate/frame/staking/src/benchmarking.rs
index c4299449196..ce4f0178a24 100644
--- a/substrate/frame/staking/src/benchmarking.rs
+++ b/substrate/frame/staking/src/benchmarking.rs
@@ -802,33 +802,21 @@ mod benchmarks {
 
 	#[benchmark]
 	fn cancel_deferred_slash(s: Linear<1, MAX_SLASHES>) {
+		let mut unapplied_slashes = Vec::new();
 		let era = EraIndex::one();
-		let dummy_account = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
-
-		// Insert `s` unapplied slashes with the new key structure
-		for i in 0..s {
-			let slash_key = (dummy_account(), Perbill::from_percent(i as u32 % 100), i);
-			let unapplied_slash = UnappliedSlash::<T> {
-				validator: slash_key.0.clone(),
-				own: Zero::zero(),
-				others: WeakBoundedVec::default(),
-				reporter: Default::default(),
-				payout: Zero::zero(),
-			};
-			UnappliedSlashes::<T>::insert(era, slash_key.clone(), unapplied_slash);
+		let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
+		for _ in 0..MAX_SLASHES {
+			unapplied_slashes
+				.push(UnappliedSlash::<T::AccountId, BalanceOf<T>>::default_from(dummy()));
 		}
+		UnappliedSlashes::<T>::insert(era, &unapplied_slashes);
 
-		let slash_keys: Vec<_> = (0..s)
-			.map(|i| (dummy_account(), Perbill::from_percent(i as u32 % 100), i))
-			.collect();
+		let slash_indices: Vec<u32> = (0..s).collect();
 
 		#[extrinsic_call]
-		_(RawOrigin::Root, era, slash_keys.clone());
+		_(RawOrigin::Root, era, slash_indices);
 
-		// Ensure all `s` slashes are removed
-		for key in &slash_keys {
-			assert!(UnappliedSlashes::<T>::get(era, key).is_none());
-		}
+		assert_eq!(UnappliedSlashes::<T>::get(&era).len(), (MAX_SLASHES - s) as usize);
 	}
 
 	#[benchmark]
@@ -1149,46 +1137,6 @@ mod benchmarks {
 		Ok(())
 	}
 
-	#[benchmark]
-	fn apply_slash() -> Result<(), BenchmarkError> {
-		let era = EraIndex::one();
-		ActiveEra::<T>::put(ActiveEraInfo { index: era, start: None });
-		let (validator, nominators) = create_validator_with_nominators::<T>(
-			T::MaxExposurePageSize::get() as u32,
-			T::MaxExposurePageSize::get() as u32,
-			false,
-			true,
-			RewardDestination::Staked,
-			era,
-		)?;
-		let slash_fraction = Perbill::from_percent(10);
-		let page_index = 0;
-		let slashed_balance = BalanceOf::<T>::from(10u32);
-
-		let slash_key = (validator.clone(), slash_fraction, page_index);
-		let slashed_nominators =
-			nominators.iter().map(|(n, _)| (n.clone(), slashed_balance)).collect::<Vec<_>>();
-
-		let unapplied_slash = UnappliedSlash::<T> {
-			validator: validator.clone(),
-			own: slashed_balance,
-			others: WeakBoundedVec::force_from(slashed_nominators, None),
-			reporter: Default::default(),
-			payout: Zero::zero(),
-		};
-
-		// Insert an unapplied slash to be processed.
-		UnappliedSlashes::<T>::insert(era, slash_key.clone(), unapplied_slash);
-
-		#[extrinsic_call]
-		_(RawOrigin::Signed(validator.clone()), era, slash_key.clone());
-
-		// Ensure the slash has been applied and removed.
-		assert!(UnappliedSlashes::<T>::get(era, &slash_key).is_none());
-
-		Ok(())
-	}
-
 	#[benchmark]
 	fn manual_slash() -> Result<(), BenchmarkError> {
 		let era = EraIndex::zero();
diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs
index 922df9f8c32..1247470edf4 100644
--- a/substrate/frame/staking/src/lib.rs
+++ b/substrate/frame/staking/src/lib.rs
@@ -353,7 +353,7 @@ use frame_support::{
 		ConstU32, Contains, Defensive, DefensiveMax, DefensiveSaturating, Get, LockIdentifier,
 	},
 	weights::Weight,
-	BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, WeakBoundedVec,
+	BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
 };
 use scale_info::TypeInfo;
 use sp_runtime::{
@@ -923,19 +923,31 @@ impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEn
 
 /// A pending slash record. The value of the slash has been computed but not applied yet,
 /// rather deferred for several eras.
-#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEqNoBound)]
-#[scale_info(skip_type_params(T))]
-pub struct UnappliedSlash<T: Config> {
+#[derive(Encode, Decode, RuntimeDebug, TypeInfo)]
+pub struct UnappliedSlash<AccountId, Balance: HasCompact> {
 	/// The stash ID of the offending validator.
-	validator: T::AccountId,
+	validator: AccountId,
 	/// The validator's own slash.
-	own: BalanceOf<T>,
+	own: Balance,
 	/// All other slashed stakers and amounts.
-	others: WeakBoundedVec<(T::AccountId, BalanceOf<T>), T::MaxExposurePageSize>,
+	others: Vec<(AccountId, Balance)>,
 	/// Reporters of the offence; bounty payout recipients.
-	reporter: Option<T::AccountId>,
+	reporters: Vec<AccountId>,
 	/// The amount of payout.
-	payout: BalanceOf<T>,
+	payout: Balance,
+}
+
+impl<AccountId, Balance: HasCompact + Zero> UnappliedSlash<AccountId, Balance> {
+	/// Initializes the default object using the given `validator`.
+	pub fn default_from(validator: AccountId) -> Self {
+		Self {
+			validator,
+			own: Zero::zero(),
+			others: vec![],
+			reporters: vec![],
+			payout: Zero::zero(),
+		}
+	}
 }
 
 /// Something that defines the maximum number of nominations per nominator based on a curve.
@@ -983,7 +995,10 @@ pub trait SessionInterface<AccountId> {
 impl<T: Config> SessionInterface<<T as frame_system::Config>::AccountId> for T
 where
 	T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
-	T: pallet_session::historical::Config,
+	T: pallet_session::historical::Config<
+		FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
+		FullIdentificationOf = ExposureOf<T>,
+	>,
 	T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>,
 	T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>,
 	T::ValidatorIdOf: Convert<
@@ -1127,13 +1142,6 @@ impl<T: Config> Convert<T::AccountId, Option<Exposure<T::AccountId, BalanceOf<T>
 	}
 }
 
-pub struct NullIdentity;
-impl<T> Convert<T, Option<()>> for NullIdentity {
-	fn convert(_: T) -> Option<()> {
-		Some(())
-	}
-}
-
 /// Filter historical offences out and only allow those from the bonding period.
 pub struct FilterHistoricalOffences<T, R> {
 	_inner: core::marker::PhantomData<(T, R)>,
diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs
index 5b0118da67e..274ed212b3a 100644
--- a/substrate/frame/staking/src/migrations.rs
+++ b/substrate/frame/staking/src/migrations.rs
@@ -18,12 +18,12 @@
 //! [CHANGELOG.md](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/staking/CHANGELOG.md).
 
 use super::*;
+use frame_election_provider_support::SortedListProvider;
 use frame_support::{
 	migrations::VersionedMigration,
 	pallet_prelude::ValueQuery,
 	storage_alias,
 	traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade},
-	Twox64Concat,
 };
 
 #[cfg(feature = "try-runtime")]
@@ -36,6 +36,10 @@ use sp_runtime::TryRuntimeError;
 /// Obsolete from v13. Keeping around to make encoding/decoding of old migration code easier.
 #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
 enum ObsoleteReleases {
+	V1_0_0Ancient,
+	V2_0_0,
+	V3_0_0,
+	V4_0_0,
 	V5_0_0,  // blockable validators.
 	V6_0_0,  // removal of all storage associated with offchain phragmen.
 	V7_0_0,  // keep track of number of nominators / validators in map
@@ -62,86 +66,6 @@ type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, Value
 pub mod v17 {
 	use super::*;
 
-	#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
-	struct OldUnappliedSlash<T: Config> {
-		validator: T::AccountId,
-		/// The validator's own slash.
-		own: BalanceOf<T>,
-		/// All other slashed stakers and amounts.
-		others: Vec<(T::AccountId, BalanceOf<T>)>,
-		/// Reporters of the offence; bounty payout recipients.
-		reporters: Vec<T::AccountId>,
-		/// The amount of payout.
-		payout: BalanceOf<T>,
-	}
-
-	#[frame_support::storage_alias]
-	pub type OldUnappliedSlashes<T: Config> =
-		StorageMap<Pallet<T>, Twox64Concat, EraIndex, Vec<OldUnappliedSlash<T>>, ValueQuery>;
-
-	#[frame_support::storage_alias]
-	pub type DisabledValidators<T: Config> =
-		StorageValue<Pallet<T>, BoundedVec<(u32, OffenceSeverity), ConstU32<100>>, ValueQuery>;
-
-	pub struct VersionUncheckedMigrateV16ToV17<T>(core::marker::PhantomData<T>);
-	impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV16ToV17<T> {
-		fn on_runtime_upgrade() -> Weight {
-			let mut weight: Weight = Weight::zero();
-
-			OldUnappliedSlashes::<T>::drain().for_each(|(era, slashes)| {
-				weight.saturating_accrue(T::DbWeight::get().reads(1));
-
-				for slash in slashes {
-					let validator = slash.validator.clone();
-					let new_slash = UnappliedSlash {
-						validator: validator.clone(),
-						own: slash.own,
-						others: WeakBoundedVec::force_from(slash.others, None),
-						payout: slash.payout,
-						reporter: slash.reporters.first().cloned(),
-					};
-
-					// creating a slash key which is improbable to conflict with a new offence.
-					let slash_key = (validator, Perbill::from_percent(99), 9999);
-					UnappliedSlashes::<T>::insert(era, slash_key, new_slash);
-					weight.saturating_accrue(T::DbWeight::get().writes(1));
-				}
-			});
-
-			weight
-		}
-
-		#[cfg(feature = "try-runtime")]
-		fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
-			let mut expected_slashes: u32 = 0;
-			OldUnappliedSlashes::<T>::iter().for_each(|(_, slashes)| {
-				expected_slashes += slashes.len() as u32;
-			});
-
-			Ok(expected_slashes.encode())
-		}
-
-		#[cfg(feature = "try-runtime")]
-		fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> {
-			let expected_slash_count =
-				u32::decode(&mut state.as_slice()).expect("Failed to decode state");
-
-			let actual_slash_count = UnappliedSlashes::<T>::iter().count() as u32;
-
-			ensure!(expected_slash_count == actual_slash_count, "Slash count mismatch");
-
-			Ok(())
-		}
-	}
-
-	pub type MigrateV16ToV17<T> = VersionedMigration<
-		16,
-		17,
-		VersionUncheckedMigrateV16ToV17<T>,
-		Pallet<T>,
-		<T as frame_system::Config>::DbWeight,
-	>;
-
 	pub struct MigrateDisabledToSession<T>(core::marker::PhantomData<T>);
 	impl<T: Config> pallet_session::migrations::v1::MigrateDisabledValidators
 		for MigrateDisabledToSession<T>
@@ -543,3 +467,257 @@ pub mod v11 {
 		}
 	}
 }
+
+pub mod v10 {
+	use super::*;
+	use frame_support::storage_alias;
+
+	#[storage_alias]
+	type EarliestUnappliedSlash<T: Config> = StorageValue<Pallet<T>, EraIndex>;
+
+	/// Apply any pending slashes that where queued.
+	///
+	/// That means we might slash someone a bit too early, but we will definitely
+	/// won't forget to slash them. The cap of 512 is somewhat randomly taken to
+	/// prevent us from iterating over an arbitrary large number of keys `on_runtime_upgrade`.
+	pub struct MigrateToV10<T>(core::marker::PhantomData<T>);
+	impl<T: Config> OnRuntimeUpgrade for MigrateToV10<T> {
+		fn on_runtime_upgrade() -> frame_support::weights::Weight {
+			if StorageVersion::<T>::get() == ObsoleteReleases::V9_0_0 {
+				let pending_slashes = UnappliedSlashes::<T>::iter().take(512);
+				for (era, slashes) in pending_slashes {
+					for slash in slashes {
+						// in the old slashing scheme, the slash era was the key at which we read
+						// from `UnappliedSlashes`.
+						log!(warn, "prematurely applying a slash ({:?}) for era {:?}", slash, era);
+						slashing::apply_slash::<T>(slash, era);
+					}
+				}
+
+				EarliestUnappliedSlash::<T>::kill();
+				StorageVersion::<T>::put(ObsoleteReleases::V10_0_0);
+
+				log!(info, "MigrateToV10 executed successfully");
+				T::DbWeight::get().reads_writes(1, 2)
+			} else {
+				log!(warn, "MigrateToV10 should be removed.");
+				T::DbWeight::get().reads(1)
+			}
+		}
+	}
+}
+
+pub mod v9 {
+	use super::*;
+	#[cfg(feature = "try-runtime")]
+	use alloc::vec::Vec;
+	#[cfg(feature = "try-runtime")]
+	use codec::{Decode, Encode};
+
+	/// Migration implementation that injects all validators into sorted list.
+	///
+	/// This is only useful for chains that started their `VoterList` just based on nominators.
+	pub struct InjectValidatorsIntoVoterList<T>(core::marker::PhantomData<T>);
+	impl<T: Config> OnRuntimeUpgrade for InjectValidatorsIntoVoterList<T> {
+		fn on_runtime_upgrade() -> Weight {
+			if StorageVersion::<T>::get() == ObsoleteReleases::V8_0_0 {
+				let prev_count = T::VoterList::count();
+				let weight_of_cached = Pallet::<T>::weight_of_fn();
+				for (v, _) in Validators::<T>::iter() {
+					let weight = weight_of_cached(&v);
+					let _ = T::VoterList::on_insert(v.clone(), weight).map_err(|err| {
+						log!(warn, "failed to insert {:?} into VoterList: {:?}", v, err)
+					});
+				}
+
+				log!(
+					info,
+					"injected a total of {} new voters, prev count: {} next count: {}, updating to version 9",
+					Validators::<T>::count(),
+					prev_count,
+					T::VoterList::count(),
+				);
+
+				StorageVersion::<T>::put(ObsoleteReleases::V9_0_0);
+				T::BlockWeights::get().max_block
+			} else {
+				log!(
+					warn,
+					"InjectValidatorsIntoVoterList being executed on the wrong storage \
+				version, expected ObsoleteReleases::V8_0_0"
+				);
+				T::DbWeight::get().reads(1)
+			}
+		}
+
+		#[cfg(feature = "try-runtime")]
+		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
+			frame_support::ensure!(
+				StorageVersion::<T>::get() == ObsoleteReleases::V8_0_0,
+				"must upgrade linearly"
+			);
+
+			let prev_count = T::VoterList::count();
+			Ok(prev_count.encode())
+		}
+
+		#[cfg(feature = "try-runtime")]
+		fn post_upgrade(prev_count: Vec<u8>) -> Result<(), TryRuntimeError> {
+			let prev_count: u32 = Decode::decode(&mut prev_count.as_slice()).expect(
+				"the state parameter should be something that was generated by pre_upgrade",
+			);
+			let post_count = T::VoterList::count();
+			let validators = Validators::<T>::count();
+			ensure!(
+				post_count == prev_count + validators,
+				"`VoterList` count after the migration must equal to the sum of \
+				previous count and the current number of validators"
+			);
+
+			frame_support::ensure!(
+				StorageVersion::<T>::get() == ObsoleteReleases::V9_0_0,
+				"must upgrade"
+			);
+			Ok(())
+		}
+	}
+}
+
+pub mod v8 {
+	use super::*;
+	use crate::{Config, Nominators, Pallet, Weight};
+	use frame_election_provider_support::SortedListProvider;
+	use frame_support::traits::Get;
+
+	#[cfg(feature = "try-runtime")]
+	pub fn pre_migrate<T: Config>() -> Result<(), &'static str> {
+		frame_support::ensure!(
+			StorageVersion::<T>::get() == ObsoleteReleases::V7_0_0,
+			"must upgrade linearly"
+		);
+
+		crate::log!(info, "👜 staking bags-list migration passes PRE migrate checks ✅",);
+		Ok(())
+	}
+
+	/// Migration to sorted `VoterList`.
+	pub fn migrate<T: Config>() -> Weight {
+		if StorageVersion::<T>::get() == ObsoleteReleases::V7_0_0 {
+			crate::log!(info, "migrating staking to ObsoleteReleases::V8_0_0");
+
+			let migrated = T::VoterList::unsafe_regenerate(
+				Nominators::<T>::iter().map(|(id, _)| id),
+				Pallet::<T>::weight_of_fn(),
+			);
+
+			StorageVersion::<T>::put(ObsoleteReleases::V8_0_0);
+			crate::log!(
+				info,
+				"👜 completed staking migration to ObsoleteReleases::V8_0_0 with {} voters migrated",
+				migrated,
+			);
+
+			T::BlockWeights::get().max_block
+		} else {
+			T::DbWeight::get().reads(1)
+		}
+	}
+
+	#[cfg(feature = "try-runtime")]
+	pub fn post_migrate<T: Config>() -> Result<(), &'static str> {
+		T::VoterList::try_state().map_err(|_| "VoterList is not in a sane state.")?;
+		crate::log!(info, "👜 staking bags-list migration passes POST migrate checks ✅",);
+		Ok(())
+	}
+}
+
+pub mod v7 {
+	use super::*;
+	use frame_support::storage_alias;
+
+	#[storage_alias]
+	type CounterForValidators<T: Config> = StorageValue<Pallet<T>, u32>;
+	#[storage_alias]
+	type CounterForNominators<T: Config> = StorageValue<Pallet<T>, u32>;
+
+	pub fn pre_migrate<T: Config>() -> Result<(), &'static str> {
+		assert!(
+			CounterForValidators::<T>::get().unwrap().is_zero(),
+			"CounterForValidators already set."
+		);
+		assert!(
+			CounterForNominators::<T>::get().unwrap().is_zero(),
+			"CounterForNominators already set."
+		);
+		assert!(Validators::<T>::count().is_zero(), "Validators already set.");
+		assert!(Nominators::<T>::count().is_zero(), "Nominators already set.");
+		assert!(StorageVersion::<T>::get() == ObsoleteReleases::V6_0_0);
+		Ok(())
+	}
+
+	pub fn migrate<T: Config>() -> Weight {
+		log!(info, "Migrating staking to ObsoleteReleases::V7_0_0");
+		let validator_count = Validators::<T>::iter().count() as u32;
+		let nominator_count = Nominators::<T>::iter().count() as u32;
+
+		CounterForValidators::<T>::put(validator_count);
+		CounterForNominators::<T>::put(nominator_count);
+
+		StorageVersion::<T>::put(ObsoleteReleases::V7_0_0);
+		log!(info, "Completed staking migration to ObsoleteReleases::V7_0_0");
+
+		T::DbWeight::get().reads_writes(validator_count.saturating_add(nominator_count).into(), 2)
+	}
+}
+
+pub mod v6 {
+	use super::*;
+	use frame_support::{storage_alias, traits::Get, weights::Weight};
+
+	// NOTE: value type doesn't matter, we just set it to () here.
+	#[storage_alias]
+	type SnapshotValidators<T: Config> = StorageValue<Pallet<T>, ()>;
+	#[storage_alias]
+	type SnapshotNominators<T: Config> = StorageValue<Pallet<T>, ()>;
+	#[storage_alias]
+	type QueuedElected<T: Config> = StorageValue<Pallet<T>, ()>;
+	#[storage_alias]
+	type QueuedScore<T: Config> = StorageValue<Pallet<T>, ()>;
+	#[storage_alias]
+	type EraElectionStatus<T: Config> = StorageValue<Pallet<T>, ()>;
+	#[storage_alias]
+	type IsCurrentSessionFinal<T: Config> = StorageValue<Pallet<T>, ()>;
+
+	/// check to execute prior to migration.
+	pub fn pre_migrate<T: Config>() -> Result<(), &'static str> {
+		// these may or may not exist.
+		log!(info, "SnapshotValidators.exits()? {:?}", SnapshotValidators::<T>::exists());
+		log!(info, "SnapshotNominators.exits()? {:?}", SnapshotNominators::<T>::exists());
+		log!(info, "QueuedElected.exits()? {:?}", QueuedElected::<T>::exists());
+		log!(info, "QueuedScore.exits()? {:?}", QueuedScore::<T>::exists());
+		// these must exist.
+		assert!(
+			IsCurrentSessionFinal::<T>::exists(),
+			"IsCurrentSessionFinal storage item not found!"
+		);
+		assert!(EraElectionStatus::<T>::exists(), "EraElectionStatus storage item not found!");
+		Ok(())
+	}
+
+	/// Migrate storage to v6.
+	pub fn migrate<T: Config>() -> Weight {
+		log!(info, "Migrating staking to ObsoleteReleases::V6_0_0");
+
+		SnapshotValidators::<T>::kill();
+		SnapshotNominators::<T>::kill();
+		QueuedElected::<T>::kill();
+		QueuedScore::<T>::kill();
+		EraElectionStatus::<T>::kill();
+		IsCurrentSessionFinal::<T>::kill();
+
+		StorageVersion::<T>::put(ObsoleteReleases::V6_0_0);
+
+		log!(info, "Done.");
+		T::DbWeight::get().writes(6 + 1)
+	}
+}
diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs
index cf1b2c7912a..b74cc24a13b 100644
--- a/substrate/frame/staking/src/mock.rs
+++ b/substrate/frame/staking/src/mock.rs
@@ -154,8 +154,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = ();
-	type FullIdentificationOf = NullIdentity;
+	type FullIdentification = crate::Exposure<AccountId, Balance>;
+	type FullIdentificationOf = crate::ExposureOf<Test>;
 }
 impl pallet_authorship::Config for Test {
 	type FindAuthor = Author11;
@@ -728,11 +728,6 @@ pub(crate) fn run_to_block(n: BlockNumber) {
 	);
 }
 
-/// Progress by n block.
-pub(crate) fn advance_blocks(n: u64) {
-	run_to_block(System::block_number() + n);
-}
-
 /// Progresses from the current block number (whatever that may be) to the `P * session_index + 1`.
 pub(crate) fn start_session(end_session_idx: SessionIndex) {
 	let period = Period::get();
@@ -835,14 +830,7 @@ pub(crate) fn on_offence_in_era(
 	>],
 	slash_fraction: &[Perbill],
 	era: EraIndex,
-	advance_processing_blocks: bool,
 ) {
-	// counter to keep track of how many blocks we need to advance to process all the offences.
-	let mut process_blocks = 0u32;
-	for detail in offenders {
-		process_blocks += EraInfo::<Test>::get_page_count(era, &detail.offender.0);
-	}
-
 	let bonded_eras = crate::BondedEras::<Test>::get();
 	for &(bonded_era, start_session) in bonded_eras.iter() {
 		if bonded_era == era {
@@ -851,9 +839,6 @@ pub(crate) fn on_offence_in_era(
 				slash_fraction,
 				start_session,
 			);
-			if advance_processing_blocks {
-				advance_blocks(process_blocks as u64);
-			}
 			return
 		} else if bonded_era > era {
 			break
@@ -866,9 +851,6 @@ pub(crate) fn on_offence_in_era(
 			slash_fraction,
 			pallet_staking::ErasStartSessionIndex::<Test>::get(era).unwrap(),
 		);
-		if advance_processing_blocks {
-			advance_blocks(process_blocks as u64);
-		}
 	} else {
 		panic!("cannot slash in era {}", era);
 	}
@@ -880,23 +862,19 @@ pub(crate) fn on_offence_now(
 		pallet_session::historical::IdentificationTuple<Test>,
 	>],
 	slash_fraction: &[Perbill],
-	advance_processing_blocks: bool,
 ) {
 	let now = pallet_staking::ActiveEra::<Test>::get().unwrap().index;
-	on_offence_in_era(offenders, slash_fraction, now, advance_processing_blocks);
-}
-pub(crate) fn offence_from(
-	offender: AccountId,
-	reporter: Option<AccountId>,
-) -> OffenceDetails<AccountId, pallet_session::historical::IdentificationTuple<Test>> {
-	OffenceDetails {
-		offender: (offender, ()),
-		reporters: reporter.map(|r| vec![(r)]).unwrap_or_default(),
-	}
+	on_offence_in_era(offenders, slash_fraction, now)
 }
 
 pub(crate) fn add_slash(who: &AccountId) {
-	on_offence_now(&[offence_from(*who, None)], &[Perbill::from_percent(10)], true);
+	on_offence_now(
+		&[OffenceDetails {
+			offender: (*who, Staking::eras_stakers(active_era(), who)),
+			reporters: vec![],
+		}],
+		&[Perbill::from_percent(10)],
+	);
 }
 
 /// Make all validator and nominator request their payment
diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs
index 10e8c679fd6..128914091cb 100644
--- a/substrate/frame/staking/src/pallet/impls.rs
+++ b/substrate/frame/staking/src/pallet/impls.rs
@@ -34,7 +34,9 @@ use frame_support::{
 use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
 use pallet_session::historical;
 use sp_runtime::{
-	traits::{Bounded, CheckedAdd, Convert, SaturatedConversion, Saturating, StaticLookup, Zero},
+	traits::{
+		Bounded, CheckedAdd, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero,
+	},
 	ArithmeticError, DispatchResult, Perbill, Percent,
 };
 use sp_staking::{
@@ -47,16 +49,15 @@ use sp_staking::{
 
 use crate::{
 	asset, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo,
-	BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, Forcing, IndividualExposure,
-	LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, MaxWinnersPerPageOf, Nominations,
-	NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, SnapshotStatus,
-	StakingLedger, ValidatorPrefs, STAKING_ID,
+	BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, ExposureOf, Forcing,
+	IndividualExposure, LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, MaxWinnersPerPageOf,
+	Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface,
+	SnapshotStatus, StakingLedger, ValidatorPrefs, STAKING_ID,
 };
 use alloc::{boxed::Box, vec, vec::Vec};
 
 use super::pallet::*;
 
-use crate::slashing::OffenceRecord;
 #[cfg(feature = "try-runtime")]
 use frame_support::ensure;
 #[cfg(any(test, feature = "try-runtime"))]
@@ -575,6 +576,8 @@ impl<T: Config> Pallet<T> {
 				}
 			}
 		});
+
+		Self::apply_unapplied_slashes(active_era);
 	}
 
 	/// Compute payout for era.
@@ -976,19 +979,17 @@ impl<T: Config> Pallet<T> {
 	}
 
 	/// Apply previously-unapplied slashes on the beginning of a new era, after a delay.
-	pub(crate) fn apply_unapplied_slashes(active_era: EraIndex) {
-		let mut slashes = UnappliedSlashes::<T>::iter_prefix(&active_era).take(1);
-		if let Some((key, slash)) = slashes.next() {
-			log!(
-				debug,
-				"🦹 found slash {:?} scheduled to be executed in era {:?}",
-				slash,
-				active_era,
-			);
-			let offence_era = active_era.saturating_sub(T::SlashDeferDuration::get());
-			slashing::apply_slash::<T>(slash, offence_era);
-			// remove the slash
-			UnappliedSlashes::<T>::remove(&active_era, &key);
+	fn apply_unapplied_slashes(active_era: EraIndex) {
+		let era_slashes = UnappliedSlashes::<T>::take(&active_era);
+		log!(
+			debug,
+			"found {} slashes scheduled to be executed in era {:?}",
+			era_slashes.len(),
+			active_era,
+		);
+		for slash in era_slashes {
+			let slash_era = active_era.saturating_sub(T::SlashDeferDuration::get());
+			slashing::apply_slash::<T>(slash, slash_era);
 		}
 	}
 
@@ -1769,23 +1770,6 @@ impl<T: Config> historical::SessionManager<T::AccountId, Exposure<T::AccountId,
 	}
 }
 
-impl<T: Config> historical::SessionManager<T::AccountId, ()> for Pallet<T> {
-	fn new_session(new_index: SessionIndex) -> Option<Vec<(T::AccountId, ())>> {
-		<Self as pallet_session::SessionManager<_>>::new_session(new_index)
-			.map(|validators| validators.into_iter().map(|v| (v, ())).collect())
-	}
-	fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<(T::AccountId, ())>> {
-		<Self as pallet_session::SessionManager<_>>::new_session_genesis(new_index)
-			.map(|validators| validators.into_iter().map(|v| (v, ())).collect())
-	}
-	fn start_session(start_index: SessionIndex) {
-		<Self as pallet_session::SessionManager<_>>::start_session(start_index)
-	}
-	fn end_session(end_index: SessionIndex) {
-		<Self as pallet_session::SessionManager<_>>::end_session(end_index)
-	}
-}
-
 /// Add reward points to block authors:
 /// * 20 points to the block producer for producing a (non-uncle) block,
 impl<T> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T>
@@ -1803,7 +1787,10 @@ impl<T: Config>
 	for Pallet<T>
 where
 	T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
-	T: pallet_session::historical::Config,
+	T: pallet_session::historical::Config<
+		FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
+		FullIdentificationOf = ExposureOf<T>,
+	>,
 	T::SessionHandler: pallet_session::SessionHandler<<T as frame_system::Config>::AccountId>,
 	T::SessionManager: pallet_session::SessionManager<<T as frame_system::Config>::AccountId>,
 	T::ValidatorIdOf: Convert<
@@ -1811,12 +1798,12 @@ where
 		Option<<T as frame_system::Config>::AccountId>,
 	>,
 {
-	/// When an offence is reported, it is split into pages and put in the offence queue.
-	/// As offence queue is processed, computed slashes are queued to be applied after the
-	/// `SlashDeferDuration`.
 	fn on_offence(
-		offenders: &[OffenceDetails<T::AccountId, historical::IdentificationTuple<T>>],
-		slash_fractions: &[Perbill],
+		offenders: &[OffenceDetails<
+			T::AccountId,
+			pallet_session::historical::IdentificationTuple<T>,
+		>],
+		slash_fraction: &[Perbill],
 		slash_session: SessionIndex,
 	) -> Weight {
 		log!(
@@ -1845,48 +1832,52 @@ impl<T: Config> Pallet<T> {
 		slash_fractions: &[Perbill],
 		slash_session: SessionIndex,
 	) -> Weight {
-		// todo(ank4n): Needs to be properly benched.
-		let mut consumed_weight = Weight::zero();
+		let reward_proportion = SlashRewardFraction::<T>::get();
+		let mut consumed_weight = Weight::from_parts(0, 0);
 		let mut add_db_reads_writes = |reads, writes| {
 			consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
 		};
 
-		// Find the era to which offence belongs.
-		add_db_reads_writes(1, 0);
-		let Some(active_era) = ActiveEra::<T>::get() else {
-			log!(warn, "🦹 on_offence: no active era; ignoring offence");
-			return consumed_weight
+		let active_era = {
+			let active_era = ActiveEra::<T>::get();
+			add_db_reads_writes(1, 0);
+			if active_era.is_none() {
+				// This offence need not be re-submitted.
+				return consumed_weight
+			}
+			active_era.expect("value checked not to be `None`; qed").index
 		};
-
+		let active_era_start_session_index = ErasStartSessionIndex::<T>::get(active_era)
+			.unwrap_or_else(|| {
+				frame_support::print("Error: start_session_index must be set for current_era");
+				0
+			});
 		add_db_reads_writes(1, 0);
-		let active_era_start_session =
-			ErasStartSessionIndex::<T>::get(active_era.index).unwrap_or(0);
+
+		let window_start = active_era.saturating_sub(T::BondingDuration::get());
 
 		// Fast path for active-era report - most likely.
 		// `slash_session` cannot be in a future active era. It must be in `active_era` or before.
-		let offence_era = if slash_session >= active_era_start_session {
-			active_era.index
+		let slash_era = if slash_session >= active_era_start_session_index {
+			active_era
 		} else {
+			let eras = BondedEras::<T>::get();
 			add_db_reads_writes(1, 0);
-			match BondedEras::<T>::get()
-				.iter()
-				// Reverse because it's more likely to find reports from recent eras.
-				.rev()
-				.find(|&(_, sesh)| sesh <= &slash_session)
-				.map(|(era, _)| *era)
-			{
-				Some(era) => era,
-				None => {
-					// defensive: this implies offence is for a discarded era, and should already be
-					// filtered out.
-					log!(warn, "🦹 on_offence: no era found for slash_session; ignoring offence");
-					return Weight::default()
-				},
+
+			// Reverse because it's more likely to find reports from recent eras.
+			match eras.iter().rev().find(|&(_, sesh)| sesh <= &slash_session) {
+				Some((slash_era, _)) => *slash_era,
+				// Before bonding period. defensive - should be filtered out.
+				None => return consumed_weight,
 			}
 		};
 
-		add_db_reads_writes(1, 0);
+		add_db_reads_writes(1, 1);
+
+		let slash_defer_duration = T::SlashDeferDuration::get();
+
 		let invulnerables = Invulnerables::<T>::get();
+		add_db_reads_writes(1, 0);
 
 		for (details, slash_fraction) in offenders.zip(slash_fractions) {
 			let validator = &details.offender;
@@ -1910,10 +1901,10 @@ impl<T: Config> Pallet<T> {
 				continue;
 			};
 
-			Self::deposit_event(Event::<T>::OffenceReported {
-				validator: validator.clone(),
+			Self::deposit_event(Event::<T>::SlashReported {
+				validator: stash.clone(),
 				fraction: *slash_fraction,
-				offence_era,
+				slash_era,
 			});
 
 			if offence_era == active_era.index {
@@ -1925,99 +1916,56 @@ impl<T: Config> Pallet<T> {
 					OffenceSeverity(*slash_fraction),
 				);
 			}
-			add_db_reads_writes(1, 0);
-			let prior_slash_fraction = ValidatorSlashInEra::<T>::get(offence_era, validator)
-				.map_or(Zero::zero(), |(f, _)| f);
 
-			add_db_reads_writes(1, 0);
-			if let Some(existing) = OffenceQueue::<T>::get(offence_era, validator) {
-				if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() {
-					add_db_reads_writes(0, 2);
-					OffenceQueue::<T>::insert(
-						offence_era,
-						validator,
-						OffenceRecord {
-							reporter: details.reporters.first().cloned(),
-							reported_era: active_era.index,
-							slash_fraction: *slash_fraction,
-							..existing
-						},
-					);
+			let unapplied = slashing::compute_slash::<T>(slashing::SlashParams {
+				stash,
+				slash: *slash_fraction,
+				exposure,
+				slash_era,
+				window_start,
+				now: active_era,
+				reward_proportion,
+			});
 
-					// update the slash fraction in the `ValidatorSlashInEra` storage.
-					ValidatorSlashInEra::<T>::insert(
-						offence_era,
-						validator,
-						(slash_fraction, exposure_overview.own),
-					);
+			if let Some(mut unapplied) = unapplied {
+				let nominators_len = unapplied.others.len() as u64;
+				let reporters_len = details.reporters.len() as u64;
 
-					log!(
-						debug,
-						"🦹 updated slash for {:?}: {:?} (prior: {:?})",
-						validator,
-						slash_fraction,
-						prior_slash_fraction,
-					);
+				{
+					let upper_bound = 1 /* Validator/NominatorSlashInEra */ + 2 /* fetch_spans */;
+					let rw = upper_bound + nominators_len * upper_bound;
+					add_db_reads_writes(rw, rw);
+				}
+				unapplied.reporters = details.reporters.clone();
+				if slash_defer_duration == 0 {
+					// Apply right away.
+					slashing::apply_slash::<T>(unapplied, slash_era);
+					{
+						let slash_cost = (6, 5);
+						let reward_cost = (2, 2);
+						add_db_reads_writes(
+							(1 + nominators_len) * slash_cost.0 + reward_cost.0 * reporters_len,
+							(1 + nominators_len) * slash_cost.1 + reward_cost.1 * reporters_len,
+						);
+					}
 				} else {
+					// Defer to end of some `slash_defer_duration` from now.
 					log!(
 						debug,
-						"🦹 ignored slash for {:?}: {:?} (existing prior is larger: {:?})",
-						validator,
+						"deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
 						slash_fraction,
-						prior_slash_fraction,
+						slash_era,
+						active_era,
+						slash_era + slash_defer_duration + 1,
+					);
+					UnappliedSlashes::<T>::mutate(
+						slash_era.saturating_add(slash_defer_duration).saturating_add(One::one()),
+						move |for_later| for_later.push(unapplied),
 					);
+					add_db_reads_writes(1, 1);
 				}
-			} else if slash_fraction.deconstruct() > prior_slash_fraction.deconstruct() {
-				add_db_reads_writes(0, 3);
-				ValidatorSlashInEra::<T>::insert(
-					offence_era,
-					validator,
-					(slash_fraction, exposure_overview.own),
-				);
-
-				OffenceQueue::<T>::insert(
-					offence_era,
-					validator,
-					OffenceRecord {
-						reporter: details.reporters.first().cloned(),
-						reported_era: active_era.index,
-						// there are cases of validator with no exposure, hence 0 page, so we
-						// saturate to avoid underflow.
-						exposure_page: exposure_overview.page_count.saturating_sub(1),
-						slash_fraction: *slash_fraction,
-						prior_slash_fraction,
-					},
-				);
-
-				OffenceQueueEras::<T>::mutate(|q| {
-					if let Some(eras) = q {
-						log!(debug, "🦹 inserting offence era {} into existing queue", offence_era);
-						eras.binary_search(&offence_era)
-							.err()
-							.map(|idx| eras.try_insert(idx, offence_era).defensive());
-					} else {
-						let mut eras = BoundedVec::default();
-						log!(debug, "🦹 inserting offence era {} into empty queue", offence_era);
-						let _ = eras.try_push(offence_era).defensive();
-						*q = Some(eras);
-					}
-				});
-
-				log!(
-					debug,
-					"🦹 queued slash for {:?}: {:?} (prior: {:?})",
-					validator,
-					slash_fraction,
-					prior_slash_fraction,
-				);
 			} else {
-				log!(
-					debug,
-					"🦹 ignored slash for {:?}: {:?} (already slashed in era with prior: {:?})",
-					validator,
-					slash_fraction,
-					prior_slash_fraction,
-				);
+				add_db_reads_writes(4 /* fetch_spans */, 5 /* kick_out_if_recent */)
 			}
 		}
 
@@ -2454,7 +2402,6 @@ impl<T: Config> Pallet<T> {
 	///
 	/// -- SHOULD ONLY BE CALLED AT THE END OF A GIVEN BLOCK.
 	pub fn ensure_snapshot_metadata_state(now: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
-		use sp_runtime::traits::One;
 		let next_election = Self::next_election_prediction(now);
 		let pages = Self::election_pages().saturated_into::<BlockNumberFor<T>>();
 		let election_prep_start = next_election - pages;
diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs
index bd035bbc0f0..076a2eb6e6a 100644
--- a/substrate/frame/staking/src/pallet/mod.rs
+++ b/substrate/frame/staking/src/pallet/mod.rs
@@ -77,7 +77,7 @@ pub mod pallet {
 	use frame_election_provider_support::{ElectionDataProvider, PageIndex};
 
 	/// The in-code storage version.
-	const STORAGE_VERSION: StorageVersion = StorageVersion::new(17);
+	const STORAGE_VERSION: StorageVersion = StorageVersion::new(16);
 
 	#[pallet::pallet]
 	#[pallet::storage_version(STORAGE_VERSION)]
@@ -649,67 +649,15 @@ pub mod pallet {
 	#[pallet::storage]
 	pub type CanceledSlashPayout<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
 
-	/// Stores reported offences in a queue until they are processed in subsequent blocks.
-	///
-	/// Each offence is recorded under the corresponding era index and the offending validator's
-	/// account. If an offence spans multiple pages, only one page is processed at a time. Offences
-	/// are handled sequentially, with their associated slashes computed and stored in
-	/// `UnappliedSlashes`. These slashes are then applied in a future era as determined by
-	/// `SlashDeferDuration`.
-	///
-	/// Any offences tied to an era older than `BondingDuration` are automatically dropped.
-	/// Processing always prioritizes the oldest era first.
-	#[pallet::storage]
-	pub type OffenceQueue<T: Config> = StorageDoubleMap<
-		_,
-		Twox64Concat,
-		EraIndex,
-		Twox64Concat,
-		T::AccountId,
-		slashing::OffenceRecord<T::AccountId>,
-	>;
-
-	/// Tracks the eras that contain offences in `OffenceQueue`, sorted from **earliest to latest**.
-	///
-	/// - This ensures efficient retrieval of the oldest offence without iterating through
-	/// `OffenceQueue`.
-	/// - When a new offence is added to `OffenceQueue`, its era is **inserted in sorted order**
-	/// if not already present.
-	/// - When all offences for an era are processed, it is **removed** from this list.
-	/// - The maximum length of this vector is bounded by `BondingDuration`.
-	///
-	/// This eliminates the need for expensive iteration and sorting when fetching the next offence
-	/// to process.
-	#[pallet::storage]
-	pub type OffenceQueueEras<T: Config> = StorageValue<_, BoundedVec<u32, T::BondingDuration>>;
-
-	/// Tracks the currently processed offence record from the `OffenceQueue`.
-	///
-	/// - When processing offences, an offence record is **popped** from the oldest era in
-	///   `OffenceQueue` and stored here.
-	/// - The function `process_offence` reads from this storage, processing one page of exposure at
-	///   a time.
-	/// - After processing a page, the `exposure_page` count is **decremented** until it reaches
-	///   zero.
-	/// - Once fully processed, the offence record is removed from this storage.
-	///
-	/// This ensures that offences are processed incrementally, preventing excessive computation
-	/// in a single block while maintaining correct slashing behavior.
-	#[pallet::storage]
-	pub type ProcessingOffence<T: Config> =
-		StorageValue<_, (EraIndex, T::AccountId, slashing::OffenceRecord<T::AccountId>)>;
-
 	/// All unapplied slashes that are queued for later.
 	#[pallet::storage]
-	pub type UnappliedSlashes<T: Config> = StorageDoubleMap<
+	#[pallet::unbounded]
+	pub type UnappliedSlashes<T: Config> = StorageMap<
 		_,
 		Twox64Concat,
 		EraIndex,
-		Twox64Concat,
-		// Unique key for unapplied slashes: (validator, slash fraction, page index).
-		(T::AccountId, Perbill, u32),
-		UnappliedSlash<T>,
-		OptionQuery,
+		Vec<UnappliedSlash<T::AccountId, BalanceOf<T>>>,
+		ValueQuery,
 	>;
 
 	/// A mapping from still-bonded eras to the first session index of that era.
@@ -978,6 +926,13 @@ pub mod pallet {
 			staker: T::AccountId,
 			amount: BalanceOf<T>,
 		},
+		/// A slash for the given validator, for the given percentage of their stake, at the given
+		/// era as been reported.
+		SlashReported {
+			validator: T::AccountId,
+			fraction: Perbill,
+			slash_era: EraIndex,
+		},
 		/// An old slashing report from a prior era was discarded because it could
 		/// not be processed.
 		OldSlashingReportDiscarded {
@@ -1061,26 +1016,6 @@ pub mod pallet {
 			page: PageIndex,
 			result: Result<u32, u32>,
 		},
-		/// An offence for the given validator, for the given percentage of their stake, at the
-		/// given era as been reported.
-		OffenceReported {
-			offence_era: EraIndex,
-			validator: T::AccountId,
-			fraction: Perbill,
-		},
-		/// An offence has been processed and the corresponding slash has been computed.
-		SlashComputed {
-			offence_era: EraIndex,
-			slash_era: EraIndex,
-			offender: T::AccountId,
-			page: u32,
-		},
-		/// An unapplied slash has been cancelled.
-		SlashCancelled {
-			slash_era: EraIndex,
-			slash_key: (T::AccountId, Perbill, u32),
-			payout: BalanceOf<T>,
-		},
 	}
 
 	#[pallet::error]
@@ -1098,8 +1033,8 @@ pub mod pallet {
 		EmptyTargets,
 		/// Duplicate index.
 		DuplicateIndex,
-		/// Slash record not found.
-		InvalidSlashRecord,
+		/// Slash record index out of bounds.
+		InvalidSlashIndex,
 		/// Cannot have a validator or nominator role, with value less than the minimum defined by
 		/// governance (see `MinValidatorBond` and `MinNominatorBond`). If unbonding is the
 		/// intention, `chill` first to remove one's role as validator/nominator.
@@ -1114,6 +1049,8 @@ pub mod pallet {
 		InvalidEraToReward,
 		/// Invalid number of nominations.
 		InvalidNumberOfNominations,
+		/// Items are not sorted and unique.
+		NotSortedAndUnique,
 		/// Rewards for this era have already been claimed for this validator.
 		AlreadyClaimed,
 		/// No nominators exist on this page.
@@ -1154,8 +1091,6 @@ pub mod pallet {
 		CannotReapStash,
 		/// The stake of this account is already migrated to `Fungible` holds.
 		AlreadyMigrated,
-		/// Era not yet started.
-		EraNotStarted,
 		/// Account is restricted from participation in staking. This may happen if the account is
 		/// staking in another way already, such as via pool.
 		Restricted,
@@ -1167,21 +1102,6 @@ pub mod pallet {
 		/// that the `ElectableStashes` has been populated with all validators from all pages at
 		/// the time of the election.
 		fn on_initialize(now: BlockNumberFor<T>) -> Weight {
-			// todo(ank4n): Hacky bench. Do it properly.
-			let mut consumed_weight = slashing::process_offence::<T>();
-
-			consumed_weight.saturating_accrue(T::DbWeight::get().reads(1));
-			if let Some(active_era) = ActiveEra::<T>::get() {
-				let max_slash_page_size = T::MaxExposurePageSize::get();
-				consumed_weight.saturating_accrue(
-					T::DbWeight::get().reads_writes(
-						3 * max_slash_page_size as u64,
-						3 * max_slash_page_size as u64,
-					),
-				);
-				Self::apply_unapplied_slashes(active_era.index);
-			}
-
 			let pages = Self::election_pages();
 
 			// election ongoing, fetch the next page.
@@ -1209,9 +1129,7 @@ pub mod pallet {
 				}
 			};
 
-			consumed_weight.saturating_accrue(inner_weight);
-
-			consumed_weight
+			T::WeightInfo::on_initialize_noop().saturating_add(inner_weight)
 		}
 
 		fn on_finalize(_n: BlockNumberFor<T>) {
@@ -1977,35 +1895,33 @@ pub mod pallet {
 			Ok(())
 		}
 
-		/// Cancels scheduled slashes for a given era before they are applied.
+		/// Cancel enactment of a deferred slash.
 		///
-		/// This function allows `T::AdminOrigin` to selectively remove pending slashes from
-		/// the `UnappliedSlashes` storage, preventing their enactment.
+		/// Can be called by the `T::AdminOrigin`.
 		///
-		/// ## Parameters
-		/// - `era`: The staking era for which slashes were deferred.
-		/// - `slash_keys`: A list of slash keys identifying the slashes to remove. This is a tuple
-		/// of `(stash, slash_fraction, page_index)`.
+		/// Parameters: era and indices of the slashes for that era to kill.
 		#[pallet::call_index(17)]
-		#[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_keys.len() as u32))]
+		#[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_indices.len() as u32))]
 		pub fn cancel_deferred_slash(
 			origin: OriginFor<T>,
 			era: EraIndex,
-			slash_keys: Vec<(T::AccountId, Perbill, u32)>,
+			slash_indices: Vec<u32>,
 		) -> DispatchResult {
 			T::AdminOrigin::ensure_origin(origin)?;
-			ensure!(!slash_keys.is_empty(), Error::<T>::EmptyTargets);
-
-			// Remove the unapplied slashes.
-			slash_keys.into_iter().for_each(|i| {
-				UnappliedSlashes::<T>::take(&era, &i).map(|unapplied_slash| {
-					Self::deposit_event(Event::<T>::SlashCancelled {
-						slash_era: era,
-						slash_key: i,
-						payout: unapplied_slash.payout,
-					});
-				});
-			});
+
+			ensure!(!slash_indices.is_empty(), Error::<T>::EmptyTargets);
+			ensure!(is_sorted_and_unique(&slash_indices), Error::<T>::NotSortedAndUnique);
+
+			let mut unapplied = UnappliedSlashes::<T>::get(&era);
+			let last_item = slash_indices[slash_indices.len() - 1];
+			ensure!((last_item as usize) < unapplied.len(), Error::<T>::InvalidSlashIndex);
+
+			for (removed, index) in slash_indices.into_iter().enumerate() {
+				let index = (index as usize) - removed;
+				unapplied.remove(index);
+			}
+
+			UnappliedSlashes::<T>::insert(&era, &unapplied);
 			Ok(())
 		}
 
@@ -2569,45 +2485,6 @@ pub mod pallet {
 			Ok(Pays::No.into())
 		}
 
-		/// Manually applies a deferred slash for a given era.
-		///
-		/// Normally, slashes are automatically applied shortly after the start of the `slash_era`.
-		/// This function exists as a **fallback mechanism** in case slashes were not applied due to
-		/// unexpected reasons. It allows anyone to manually apply an unapplied slash.
-		///
-		/// ## Parameters
-		/// - `slash_era`: The staking era in which the slash was originally scheduled.
-		/// - `slash_key`: A unique identifier for the slash, represented as a tuple:
-		///   - `stash`: The stash account of the validator being slashed.
-		///   - `slash_fraction`: The fraction of the stake that was slashed.
-		///   - `page_index`: The index of the exposure page being processed.
-		///
-		/// ## Behavior
-		/// - The function is **permissionless**—anyone can call it.
-		/// - The `slash_era` **must be the current era or a past era**. If it is in the future, the
-		///   call fails with `EraNotStarted`.
-		/// - The fee is waived if the slash is successfully applied.
-		///
-		/// ## TODO: Future Improvement
-		/// - Implement an **off-chain worker (OCW) task** to automatically apply slashes when there
-		///   is unused block space, improving efficiency.
-		#[pallet::call_index(31)]
-		#[pallet::weight(T::WeightInfo::apply_slash())]
-		pub fn apply_slash(
-			origin: OriginFor<T>,
-			slash_era: EraIndex,
-			slash_key: (T::AccountId, Perbill, u32),
-		) -> DispatchResultWithPostInfo {
-			let _ = ensure_signed(origin)?;
-			let active_era = ActiveEra::<T>::get().map(|a| a.index).unwrap_or_default();
-			ensure!(slash_era <= active_era, Error::<T>::EraNotStarted);
-			let unapplied_slash = UnappliedSlashes::<T>::take(&slash_era, &slash_key)
-				.ok_or(Error::<T>::InvalidSlashRecord)?;
-			slashing::apply_slash::<T>(unapplied_slash, slash_era);
-
-			Ok(Pays::No.into())
-		}
-
 		/// This function allows governance to manually slash a validator and is a
 		/// **fallback mechanism**.
 		///
diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs
index 30d4197a888..f31669756e2 100644
--- a/substrate/frame/staking/src/slashing.rs
+++ b/substrate/frame/staking/src/slashing.rs
@@ -58,12 +58,12 @@ use alloc::vec::Vec;
 use codec::{Decode, Encode, MaxEncodedLen};
 use frame_support::{
 	ensure,
-	traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced},
+	traits::{Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
 };
 use scale_info::TypeInfo;
 use sp_runtime::{
 	traits::{Saturating, Zero},
-	DispatchResult, RuntimeDebug, WeakBoundedVec, Weight,
+	DispatchResult, RuntimeDebug,
 };
 use sp_staking::{EraIndex, StakingInterface};
 
@@ -209,12 +209,8 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> {
 	pub(crate) stash: &'a T::AccountId,
 	/// The proportion of the slash.
 	pub(crate) slash: Perbill,
-	/// The prior slash proportion of the validator if the validator has been reported multiple
-	/// times in the same era, and a new greater slash replaces the old one.
-	/// Invariant: slash > prior_slash
-	pub(crate) prior_slash: Perbill,
 	/// The exposure of the stash and all nominators.
-	pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
+	pub(crate) exposure: &'a Exposure<T::AccountId, BalanceOf<T>>,
 	/// The era where the offence occurred.
 	pub(crate) slash_era: EraIndex,
 	/// The first era in the current bonding period.
@@ -226,248 +222,78 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> {
 	pub(crate) reward_proportion: Perbill,
 }
 
-/// Represents an offence record within the staking system, capturing details about a slashing
-/// event.
-#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebug)]
-pub struct OffenceRecord<AccountId> {
-	/// The account ID of the entity that reported the offence.
-	pub reporter: Option<AccountId>,
-
-	/// Era at which the offence was reported.
-	pub reported_era: EraIndex,
-
-	/// The specific page of the validator's exposure currently being processed.
-	///
-	/// Since a validator's total exposure can span multiple pages, this field serves as a pointer
-	/// to the current page being evaluated. The processing order starts from the last page
-	/// and moves backward, decrementing this value with each processed page.
-	///
-	/// This ensures that all pages are systematically handled, and it helps track when
-	/// the entire exposure has been processed.
-	pub exposure_page: u32,
-
-	/// The fraction of the validator's stake to be slashed for this offence.
-	pub slash_fraction: Perbill,
-
-	/// The previous slash fraction of the validator's stake before being updated.
-	/// If a new, higher slash fraction is reported, this field stores the prior fraction
-	/// that was overwritten. This helps in tracking changes in slashes across multiple reports for
-	/// the same era.
-	pub prior_slash_fraction: Perbill,
-}
-
-/// Loads next offence in the processing offence and returns the offense record to be processed.
+/// Computes a slash of a validator and nominators. It returns an unapplied
+/// record to be applied at some later point. Slashing metadata is updated in storage,
+/// since unapplied records are only rarely intended to be dropped.
 ///
-/// Note: this can mutate the following storage
-/// - `ProcessingOffence`
-/// - `OffenceQueue`
-/// - `OffenceQueueEras`
-fn next_offence<T: Config>() -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
-	let processing_offence = ProcessingOffence::<T>::get();
-
-	if let Some((offence_era, offender, offence_record)) = processing_offence {
-		// If the exposure page is 0, then the offence has been processed.
-		if offence_record.exposure_page == 0 {
-			ProcessingOffence::<T>::kill();
-			return Some((offence_era, offender, offence_record))
-		}
-
-		// Update the next page.
-		ProcessingOffence::<T>::put((
-			offence_era,
-			&offender,
-			OffenceRecord {
-				// decrement the page index.
-				exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
-				..offence_record.clone()
-			},
-		));
-
-		return Some((offence_era, offender, offence_record))
-	}
-
-	// Nothing in processing offence. Try to enqueue the next offence.
-	let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
-	let Some(&oldest_era) = eras.first() else { return None };
-
-	let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
-	let next_offence = offence_iter.next();
-
-	if let Some((ref validator, ref offence_record)) = next_offence {
-		// Update the processing offence if the offence is multi-page.
-		if offence_record.exposure_page > 0 {
-			// update processing offence with the next page.
-			ProcessingOffence::<T>::put((
-				oldest_era,
-				validator.clone(),
-				OffenceRecord {
-					exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
-					..offence_record.clone()
-				},
-			));
-		}
-
-		// Remove from `OffenceQueue`
-		OffenceQueue::<T>::remove(oldest_era, &validator);
-	}
+/// The pending slash record returned does not have initialized reporters. Those have
+/// to be set at a higher level, if any.
+pub(crate) fn compute_slash<T: Config>(
+	params: SlashParams<T>,
+) -> Option<UnappliedSlash<T::AccountId, BalanceOf<T>>> {
+	let mut reward_payout = Zero::zero();
+	let mut val_slashed = Zero::zero();
 
-	// If there are no offences left for the era, remove the era from `OffenceQueueEras`.
-	if offence_iter.next().is_none() {
-		if eras.len() == 1 {
-			// If there is only one era left, remove the entire queue.
-			OffenceQueueEras::<T>::kill();
-		} else {
-			// Remove the oldest era
-			eras.remove(0);
-			OffenceQueueEras::<T>::put(eras);
-		}
+	// is the slash amount here a maximum for the era?
+	let own_slash = params.slash * params.exposure.own;
+	if params.slash * params.exposure.total == Zero::zero() {
+		// kick out the validator even if they won't be slashed,
+		// as long as the misbehavior is from their most recent slashing span.
+		kick_out_if_recent::<T>(params);
+		return None
 	}
 
-	next_offence.map(|(v, o)| (oldest_era, v, o))
-}
+	let prior_slash_p = ValidatorSlashInEra::<T>::get(&params.slash_era, params.stash)
+		.map_or(Zero::zero(), |(prior_slash_proportion, _)| prior_slash_proportion);
 
-/// Infallible function to process an offence.
-pub(crate) fn process_offence<T: Config>() -> Weight {
-	// todo(ank4n): this needs to be properly benched.
-	let mut consumed_weight = Weight::from_parts(0, 0);
-	let mut add_db_reads_writes = |reads, writes| {
-		consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
-	};
-
-	add_db_reads_writes(1, 1);
-	let Some((offence_era, offender, offence_record)) = next_offence::<T>() else {
-		return consumed_weight
-	};
-
-	log!(
-		debug,
-		"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
-		offender,
-		offence_era,
-		offence_record.slash_fraction,
-	);
-
-	add_db_reads_writes(1, 0);
-	let reward_proportion = SlashRewardFraction::<T>::get();
-
-	add_db_reads_writes(2, 0);
-	let Some(exposure) =
-		EraInfo::<T>::get_paged_exposure(offence_era, &offender, offence_record.exposure_page)
-	else {
-		// this can only happen if the offence was valid at the time of reporting but became too old
-		// at the time of computing and should be discarded.
-		return consumed_weight
-	};
-
-	let slash_page = offence_record.exposure_page;
-	let slash_defer_duration = T::SlashDeferDuration::get();
-	let slash_era = offence_era.saturating_add(slash_defer_duration);
-	let window_start = offence_record.reported_era.saturating_sub(T::BondingDuration::get());
-
-	add_db_reads_writes(3, 3);
-	let Some(mut unapplied) = compute_slash::<T>(SlashParams {
-		stash: &offender,
-		slash: offence_record.slash_fraction,
-		prior_slash: offence_record.prior_slash_fraction,
-		exposure: &exposure,
-		slash_era: offence_era,
-		window_start,
-		now: offence_record.reported_era,
-		reward_proportion,
-	}) else {
-		log!(
-			debug,
-			"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
-			offence_record.slash_fraction,
-			offence_era,
-			offence_record.reported_era,
+	// compare slash proportions rather than slash values to avoid issues due to rounding
+	// error.
+	if params.slash.deconstruct() > prior_slash_p.deconstruct() {
+		ValidatorSlashInEra::<T>::insert(
+			&params.slash_era,
+			params.stash,
+			&(params.slash, own_slash),
 		);
-		// No slash to apply. Discard.
-		return consumed_weight
-	};
-
-	<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
-		offence_era,
-		slash_era,
-		offender: offender.clone(),
-		page: slash_page,
-	});
-
-	log!(
-		debug,
-		"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
-		offence_record.slash_fraction,
-		offence_era,
-		offence_record.reported_era,
-	);
+	} else {
+		// we slash based on the max in era - this new event is not the max,
+		// so neither the validator or any nominators will need an update.
+		//
+		// this does lead to a divergence of our system from the paper, which
+		// pays out some reward even if the latest report is not max-in-era.
+		// we opt to avoid the nominator lookups and edits and leave more rewards
+		// for more drastic misbehavior.
+		return None
+	}
 
-	// add the reporter to the unapplied slash.
-	unapplied.reporter = offence_record.reporter;
-
-	if slash_defer_duration == 0 {
-		// Apply right away.
-		log!(
-			debug,
-			"🦹 applying slash instantly of {:?}% happened in {:?} (reported in {:?}) to {:?}",
-			offence_record.slash_fraction,
-			offence_era,
-			offence_record.reported_era,
-			offender,
+	// apply slash to validator.
+	{
+		let mut spans = fetch_spans::<T>(
+			params.stash,
+			params.window_start,
+			&mut reward_payout,
+			&mut val_slashed,
+			params.reward_proportion,
 		);
 
-		let accounts_slashed = unapplied.others.len() as u64 + 1;
-		add_db_reads_writes(3 * accounts_slashed, 3 * accounts_slashed);
-		apply_slash::<T>(unapplied, offence_era);
-	} else {
-		// Historical Note: Previously, with BondingDuration = 28 and SlashDeferDuration = 27,
-		// slashes were applied at the start of the 28th era from `offence_era`.
-		// However, with paged slashing, applying slashes now takes multiple blocks.
-		// To account for this delay, slashes are now applied at the start of the 27th era from
-		// `offence_era`.
-		log!(
-			debug,
-			"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
-			offence_record.slash_fraction,
-			offence_era,
-			offence_record.reported_era,
-			slash_era,
-		);
+		let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
 
-		add_db_reads_writes(0, 1);
-		UnappliedSlashes::<T>::insert(
-			slash_era,
-			(offender, offence_record.slash_fraction, slash_page),
-			unapplied,
-		);
+		if target_span == Some(spans.span_index()) {
+			// misbehavior occurred within the current slashing span - end current span.
+			// Check <https://github.com/paritytech/polkadot-sdk/issues/2650> for details.
+			spans.end_span(params.now);
+		}
 	}
 
-	consumed_weight
-}
-
-/// Computes a slash of a validator and nominators. It returns an unapplied
-/// record to be applied at some later point. Slashing metadata is updated in storage,
-/// since unapplied records are only rarely intended to be dropped.
-///
-/// The pending slash record returned does not have initialized reporters. Those have
-/// to be set at a higher level, if any.
-///
-/// If `nomintors_only` is set to `true`, only the nominator slashes will be computed.
-pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> {
-	let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone());
+	add_offending_validator::<T>(&params);
 
 	let mut nominators_slashed = Vec::new();
-	let (nom_slashed, nom_reward_payout) =
-		slash_nominators::<T>(params.clone(), &mut nominators_slashed);
-	reward_payout += nom_reward_payout;
+	reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed);
 
-	(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
+	Some(UnappliedSlash {
 		validator: params.stash.clone(),
 		own: val_slashed,
-		others: WeakBoundedVec::force_from(
-			nominators_slashed,
-			Some("slashed nominators not expected to be larger than the bounds"),
-		),
-		reporter: None,
+		others: nominators_slashed,
+		reporters: Vec::new(),
 		payout: reward_payout,
 	})
 }
@@ -490,6 +316,64 @@ fn kick_out_if_recent<T: Config>(params: SlashParams<T>) {
 		// Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details
 		spans.end_span(params.now);
 	}
+
+	add_offending_validator::<T>(&params);
+}
+
+/// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of
+/// validators provided by [`decision`].
+pub(crate) fn add_offending_validator<T: Config>(
+	stash: &T::AccountId,
+	slash: Perbill,
+	offence_era: EraIndex,
+) {
+	DisabledValidators::<T>::mutate(|disabled| {
+		let new_severity = OffenceSeverity(slash);
+		let decision = T::DisablingStrategy::decision(stash, new_severity, offence_era, &disabled);
+
+		if let Some(offender_idx) = decision.disable {
+			// Check if the offender is already disabled
+			match disabled.binary_search_by_key(&offender_idx, |(index, _)| *index) {
+				// Offender is already disabled, update severity if the new one is higher
+				Ok(index) => {
+					let (_, old_severity) = &mut disabled[index];
+					if new_severity > *old_severity {
+						*old_severity = new_severity;
+					}
+				},
+				Err(index) => {
+					// Offender is not disabled, add to `DisabledValidators` and disable it
+					if disabled.try_insert(index, (offender_idx, new_severity)).defensive().is_ok()
+					{
+						// Propagate disablement to session level
+						T::SessionInterface::disable_validator(offender_idx);
+						// Emit event that a validator got disabled
+						<Pallet<T>>::deposit_event(super::Event::<T>::ValidatorDisabled {
+							stash: stash.clone(),
+						});
+					}
+				},
+			}
+		}
+
+		if let Some(reenable_idx) = decision.reenable {
+			// Remove the validator from `DisabledValidators` and re-enable it.
+			if let Ok(index) = disabled.binary_search_by_key(&reenable_idx, |(index, _)| *index) {
+				disabled.remove(index);
+				// Propagate re-enablement to session level
+				T::SessionInterface::enable_validator(reenable_idx);
+				// Emit event that a validator got re-enabled
+				let reenabled_stash =
+					T::SessionInterface::validators()[reenable_idx as usize].clone();
+				<Pallet<T>>::deposit_event(super::Event::<T>::ValidatorReenabled {
+					stash: reenabled_stash,
+				});
+			}
+		}
+	});
+
+	// `DisabledValidators` should be kept sorted
+	debug_assert!(DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] < pair[1]));
 }
 
 /// Compute the slash for a validator. Returns the amount slashed and the reward payout.
@@ -539,23 +423,23 @@ fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceO
 
 /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
 ///
-/// Returns the total amount slashed and amount of reward to pay out.
+/// Returns the amount of reward to pay out.
 fn slash_nominators<T: Config>(
 	params: SlashParams<T>,
+	prior_slash_p: Perbill,
 	nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
-) -> (BalanceOf<T>, BalanceOf<T>) {
-	let mut reward_payout = BalanceOf::<T>::zero();
-	let mut total_slashed = BalanceOf::<T>::zero();
+) -> BalanceOf<T> {
+	let mut reward_payout = Zero::zero();
 
-	nominators_slashed.reserve(params.exposure.exposure_page.others.len());
-	for nominator in &params.exposure.exposure_page.others {
+	nominators_slashed.reserve(params.exposure.others.len());
+	for nominator in &params.exposure.others {
 		let stash = &nominator.who;
 		let mut nom_slashed = Zero::zero();
 
-		// the era slash of a nominator always grows, if the validator had a new max slash for the
-		// era.
+		// the era slash of a nominator always grows, if the validator
+		// had a new max slash for the era.
 		let era_slash = {
-			let own_slash_prior = params.prior_slash * nominator.value;
+			let own_slash_prior = prior_slash_p * nominator.value;
 			let own_slash_by_validator = params.slash * nominator.value;
 			let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior);
 
@@ -585,10 +469,9 @@ fn slash_nominators<T: Config>(
 			}
 		}
 		nominators_slashed.push((stash.clone(), nom_slashed));
-		total_slashed.saturating_accrue(nom_slashed);
 	}
 
-	(total_slashed, reward_payout)
+	reward_payout
 }
 
 // helper struct for managing a set of spans we are currently inspecting.
@@ -802,25 +685,22 @@ pub fn do_slash<T: Config>(
 }
 
 /// Apply a previously-unapplied slash.
-pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
+pub(crate) fn apply_slash<T: Config>(
+	unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>,
+	slash_era: EraIndex,
+) {
 	let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
 	let mut reward_payout = unapplied_slash.payout;
 
-	if unapplied_slash.own > Zero::zero() {
-		do_slash::<T>(
-			&unapplied_slash.validator,
-			unapplied_slash.own,
-			&mut reward_payout,
-			&mut slashed_imbalance,
-			slash_era,
-		);
-	}
+	do_slash::<T>(
+		&unapplied_slash.validator,
+		unapplied_slash.own,
+		&mut reward_payout,
+		&mut slashed_imbalance,
+		slash_era,
+	);
 
 	for &(ref nominator, nominator_slash) in &unapplied_slash.others {
-		if nominator_slash.is_zero() {
-			continue
-		}
-
 		do_slash::<T>(
 			nominator,
 			nominator_slash,
@@ -830,11 +710,7 @@ pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_e
 		);
 	}
 
-	pay_reporters::<T>(
-		reward_payout,
-		slashed_imbalance,
-		&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
-	);
+	pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
 }
 
 /// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance.
diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs
index 554c705bfbb..d6b4b9435b5 100644
--- a/substrate/frame/staking/src/tests.rs
+++ b/substrate/frame/staking/src/tests.rs
@@ -49,7 +49,7 @@ use sp_runtime::{
 };
 use sp_staking::{
 	offence::{OffenceDetails, OnOffenceHandler},
-	SessionIndex, StakingInterface,
+	SessionIndex,
 };
 use substrate_test_utils::assert_eq_uvec;
 
@@ -753,7 +753,10 @@ fn nominators_also_get_slashed_pro_rata() {
 			let exposed_nominator = initial_exposure.others.first().unwrap().value;
 
 			// 11 goes offline
-			on_offence_now(&[offence_from(11, None)], &[slash_percent], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
+				&[slash_percent],
+			);
 
 			// both stakes must have been decreased.
 			assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake);
@@ -2450,7 +2453,13 @@ fn reward_validator_slashing_validator_does_not_overflow() {
 		);
 
 		// Check slashing
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(100)],
+		);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), stake - 1);
 		assert_eq!(asset::stakeable_balance::<Test>(&2), 1);
@@ -2543,7 +2552,13 @@ fn era_is_always_same_length() {
 #[test]
 fn offence_doesnt_force_new_era() {
 	ExtBuilder::default().build_and_execute(|| {
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(5)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(5)],
+		);
 
 		assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 	});
@@ -2555,7 +2570,13 @@ fn offence_ensures_new_era_without_clobbering() {
 		assert_ok!(Staking::force_new_era_always(RuntimeOrigin::root()));
 		assert_eq!(ForceEra::<Test>::get(), Forcing::ForceAlways);
 
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(5)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(5)],
+		);
 
 		assert_eq!(ForceEra::<Test>::get(), Forcing::ForceAlways);
 	});
@@ -2573,7 +2594,13 @@ fn offence_deselects_validator_even_when_slash_is_zero() {
 			assert!(Session::validators().contains(&11));
 			assert!(<Validators<Test>>::contains_key(11));
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
+			on_offence_now(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				&[Perbill::from_percent(0)],
+			);
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 			assert!(is_disabled(11));
@@ -2593,10 +2620,16 @@ fn slashing_performed_according_exposure() {
 		assert_eq!(Staking::eras_stakers(active_era(), &11).own, 1000);
 
 		// Handle an offence with a historical exposure.
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true);
+		on_offence_now(
+			&[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!(asset::stakeable_balance::<Test>(&11), 1000 / 2);
+		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000 - 250);
 	});
 }
 
@@ -2611,7 +2644,13 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() {
 			assert!(<Validators<Test>>::contains_key(11));
 			assert!(Session::validators().contains(&11));
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
+			on_offence_now(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				&[Perbill::from_percent(0)],
+			);
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 			assert!(is_disabled(11));
@@ -2627,7 +2666,14 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() {
 			mock::start_active_era(3);
 
 			// an offence committed in era 1 is reported in era 3
-			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(0)], 1, true);
+			on_offence_in_era(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				&[Perbill::from_percent(0)],
+				1,
+			);
 
 			// the validator doesn't get disabled for an old offence
 			assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
@@ -2637,11 +2683,13 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() {
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
 			on_offence_in_era(
-				&[offence_from(11, None)],
+				&[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)],
 				1,
-				true,
 			);
 
 			// the validator doesn't get disabled again
@@ -2653,9 +2701,9 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() {
 }
 
 #[test]
-fn only_first_reporter_receive_the_slice() {
-	// This test verifies that the first reporter of the offence receive their slice from the
-	// slashed amount.
+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.
 		let initial_balance = 1125;
@@ -2663,16 +2711,19 @@ fn only_first_reporter_receive_the_slice() {
 		assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance);
 
 		on_offence_now(
-			&[OffenceDetails { offender: (11, ()), reporters: vec![1, 2] }],
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![1, 2],
+			}],
 			&[Perbill::from_percent(50)],
-			true,
 		);
 
 		// F1 * (reward_proportion * slash - 0)
 		// 50% * (10% * initial_balance / 2)
 		let reward = (initial_balance / 20) / 2;
-		assert_eq!(asset::total_balance::<Test>(&1), 10 + reward);
-		assert_eq!(asset::total_balance::<Test>(&2), 20 + 0);
+		let reward_each = reward / 2; // split into two pieces.
+		assert_eq!(asset::total_balance::<Test>(&1), 10 + reward_each);
+		assert_eq!(asset::total_balance::<Test>(&2), 20 + reward_each);
 	});
 }
 
@@ -2686,14 +2737,26 @@ fn subsequent_reports_in_same_span_pay_out_less() {
 
 		assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance);
 
-		on_offence_now(&[offence_from(11, Some(1))], &[Perbill::from_percent(20)], true);
+		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!(asset::total_balance::<Test>(&1), 10 + reward);
 
-		on_offence_now(&[offence_from(11, Some(1))], &[Perbill::from_percent(50)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![1],
+			}],
+			&[Perbill::from_percent(50)],
+		);
 
 		let prior_payout = reward;
 
@@ -2721,9 +2784,17 @@ fn invulnerables_are_not_slashed() {
 			.collect();
 
 		on_offence_now(
-			&[offence_from(11, None), offence_from(21, None)],
+			&[
+				OffenceDetails {
+					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)],
-			true,
 		);
 
 		// The validator 11 hasn't been slashed, but 21 has been.
@@ -2747,7 +2818,13 @@ fn dont_slash_if_fraction_is_zero() {
 	ExtBuilder::default().build_and_execute(|| {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
+		on_offence_now(
+			&[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!(asset::stakeable_balance::<Test>(&11), 1000);
@@ -2762,18 +2839,36 @@ fn only_slash_for_max_in_era() {
 	ExtBuilder::default().build_and_execute(|| {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(50)],
+		);
 
 		// The validator has been slashed and has been force-chilled.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 500);
 		assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(25)],
+		);
 
 		// The validator has not been slashed additionally.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 500);
 
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(60)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(60)],
+		);
 
 		// The validator got slashed 10% more.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 400);
@@ -2789,13 +2884,25 @@ fn garbage_collection_after_slashing() {
 		.build_and_execute(|| {
 			assert_eq!(asset::stakeable_balance::<Test>(&11), 2000);
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
+			on_offence_now(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				&[Perbill::from_percent(10)],
+			);
 
 			assert_eq!(asset::stakeable_balance::<Test>(&11), 2000 - 200);
 			assert!(SlashingSpans::<Test>::get(&11).is_some());
 			assert_eq!(SpanSlash::<Test>::get(&(11, 0)).amount(), &200);
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true);
+			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.
@@ -2833,7 +2940,13 @@ fn garbage_collection_on_window_pruning() {
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
-		add_slash(&11);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(now, &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+		);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 900);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000 - (nominated_value / 10));
@@ -2871,7 +2984,14 @@ fn slashing_nominators_by_span_max() {
 		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(&[offence_from(11, None)], &[Perbill::from_percent(10)], 2, true);
+		on_offence_in_era(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+			2,
+		);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 900);
 
@@ -2890,7 +3010,14 @@ fn slashing_nominators_by_span_max() {
 		assert_eq!(get_span(101).iter().collect::<Vec<_>>(), expected_spans);
 
 		// second slash: higher era, higher value, same span.
-		on_offence_in_era(&[offence_from(21, None)], &[Perbill::from_percent(30)], 3, true);
+		on_offence_in_era(
+			&[OffenceDetails {
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(30)],
+			3,
+		);
 
 		// 11 was not further slashed, but 21 and 101 were.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 900);
@@ -2904,7 +3031,14 @@ fn slashing_nominators_by_span_max() {
 
 		// 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(&[offence_from(11, None)], &[Perbill::from_percent(20)], 2, true);
+		on_offence_in_era(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(20)],
+			2,
+		);
 
 		// 11 was further slashed, but 21 and 101 were not.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 800);
@@ -2931,7 +3065,13 @@ fn slashes_are_summed_across_spans() {
 
 		let get_span = |account| SlashingSpans::<Test>::get(&account).unwrap();
 
-		on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+		);
 
 		let expected_spans = vec![
 			slashing::SlashingSpan { index: 1, start: 4, length: None },
@@ -2948,7 +3088,13 @@ fn slashes_are_summed_across_spans() {
 
 		assert_eq!(Staking::slashable_balance_of(&21), 900);
 
-		on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+		);
 
 		let expected_spans = vec![
 			slashing::SlashingSpan { index: 2, start: 5, length: None },
@@ -2974,10 +3120,13 @@ fn deferred_slashes_are_deferred() {
 
 		System::reset_events();
 
-		// only 1 page of exposure, so slashes will be applied in one block.
-		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 1);
-
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+		);
 
 		// nominations are not removed regardless of the deferring.
 		assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -2990,37 +3139,27 @@ fn deferred_slashes_are_deferred() {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
-		assert!(matches!(
-			staking_events_since_last_call().as_slice(),
-			&[
-				Event::OffenceReported { validator: 11, offence_era: 1, .. },
-				Event::SlashComputed { offence_era: 1, slash_era: 3, page: 0, .. },
-				Event::PagedElectionProceeded { page: 0, result: Ok(2) },
-				Event::StakersElected,
-				..,
-			]
-		));
-
-		// the slashes for era 1 will start applying in era 3, to end before era 4.
 		mock::start_active_era(3);
-		// Slashes not applied yet. Will apply in the next block after era starts.
+
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
-		// trigger slashing by advancing block.
-		advance_blocks(1);
+
+		// 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!(asset::stakeable_balance::<Test>(&11), 900);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000 - (nominated_value / 10));
 
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				// era 3 elections
+				Event::SlashReported { validator: 11, slash_era: 1, .. },
 				Event::PagedElectionProceeded { page: 0, result: Ok(2) },
 				Event::StakersElected,
-				Event::EraPaid { .. },
-				// slashes applied from era 1 between era 3 and 4.
+				..,
 				Event::Slashed { staker: 11, amount: 100 },
-				Event::Slashed { staker: 101, amount: 12 },
+				Event::Slashed { staker: 101, amount: 12 }
 			]
 		));
 	})
@@ -3032,26 +3171,25 @@ fn retroactive_deferred_slashes_two_eras_before() {
 		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!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
 
 		System::reset_events();
 		on_offence_in_era(
-			&[offence_from(11, None)],
+			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
 			&[Perbill::from_percent(10)],
-			1, // should be deferred for two eras, and applied at the beginning of era 3.
-			true,
+			1, // should be deferred for two full eras, and applied at the beginning of era 4.
 		);
 
-		mock::start_active_era(3);
-		// Slashes not applied yet. Will apply in the next block after era starts.
-		advance_blocks(1);
+		mock::start_active_era(4);
 
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::OffenceReported { validator: 11, offence_era: 1, .. },
-				Event::SlashComputed { offence_era: 1, slash_era: 3, offender: 11, page: 0 },
+				Event::SlashReported { validator: 11, slash_era: 1, .. },
 				..,
 				Event::Slashed { staker: 11, amount: 100 },
 				Event::Slashed { staker: 101, amount: 12 }
@@ -3065,6 +3203,9 @@ 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(11)));
@@ -3073,23 +3214,21 @@ fn retroactive_deferred_slashes_one_before() {
 		mock::start_active_era(3);
 		System::reset_events();
 		on_offence_in_era(
-			&[offence_from(11, None)],
+			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
 			&[Perbill::from_percent(10)],
-			2, // should be deferred for two eras, and applied before the beginning of era 4.
-			true,
+			2, // should be deferred for two full eras, and applied at the beginning of era 5.
 		);
 
 		mock::start_active_era(4);
 
 		assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000);
-		// slash happens at next blocks.
-		advance_blocks(1);
+		// slash happens after the next line.
 
+		mock::start_active_era(5);
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::OffenceReported { validator: 11, offence_era: 2, .. },
-				Event::SlashComputed { offence_era: 2, slash_era: 4, offender: 11, page: 0 },
+				Event::SlashReported { validator: 11, slash_era: 2, .. },
 				..,
 				Event::Slashed { staker: 11, amount: 100 },
 				Event::Slashed { staker: 101, amount: 12 }
@@ -3115,7 +3254,13 @@ fn staker_cannot_bail_deferred_slash() {
 		let exposure = Staking::eras_stakers(active_era(), &11);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
-		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+		);
 
 		// now we chill
 		assert_ok!(Staking::chill(RuntimeOrigin::signed(101)));
@@ -3184,44 +3329,23 @@ fn remove_deferred() {
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
-		// deferred to start of era 3.
-		let slash_fraction_one = Perbill::from_percent(10);
-		on_offence_now(&[offence_from(11, None)], &[slash_fraction_one], true);
+		// deferred to start of era 4.
+		on_offence_now(
+			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
+			&[Perbill::from_percent(10)],
+		);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
 		mock::start_active_era(2);
 
-		// reported later, but deferred to start of era 3 as well.
+		// reported later, but deferred to start of era 4 as well.
 		System::reset_events();
-		let slash_fraction_two = Perbill::from_percent(15);
-		on_offence_in_era(&[offence_from(11, None)], &[slash_fraction_two], 1, true);
-
-		assert_eq!(
-			UnappliedSlashes::<Test>::iter_prefix(&3).collect::<Vec<_>>(),
-			vec![
-				(
-					(11, slash_fraction_one, 0),
-					UnappliedSlash {
-						validator: 11,
-						own: 100,
-						others: bounded_vec![(101, 12)],
-						reporter: None,
-						payout: 5
-					}
-				),
-				(
-					(11, slash_fraction_two, 0),
-					UnappliedSlash {
-						validator: 11,
-						own: 50,
-						others: bounded_vec![(101, 7)],
-						reporter: None,
-						payout: 6
-					}
-				),
-			]
+		on_offence_in_era(
+			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
+			&[Perbill::from_percent(15)],
+			1,
 		);
 
 		// fails if empty
@@ -3230,13 +3354,8 @@ fn remove_deferred() {
 			Error::<Test>::EmptyTargets
 		);
 
-		// cancel the slash with 10%.
-		assert_ok!(Staking::cancel_deferred_slash(
-			RuntimeOrigin::root(),
-			3,
-			vec![(11, slash_fraction_one, 0)]
-		));
-		assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&3).count(), 1);
+		// cancel one of them.
+		assert_ok!(Staking::cancel_deferred_slash(RuntimeOrigin::root(), 4, vec![0]));
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
@@ -3246,29 +3365,23 @@ fn remove_deferred() {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
-		// at the next blocks, slashes from era 1 are processed, 1 page a block,
-		// after being deferred for 2 eras.
-		advance_blocks(1);
+		// 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);
 
 		// the first slash for 10% was cancelled, but the 15% one not.
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::OffenceReported { validator: 11, offence_era: 1, .. },
-				Event::SlashComputed { offence_era: 1, slash_era: 3, offender: 11, page: 0 },
-				Event::SlashCancelled {
-					slash_era: 3,
-					slash_key: (11, fraction, 0),
-					payout: 5
-				},
+				Event::SlashReported { validator: 11, slash_era: 1, .. },
 				..,
 				Event::Slashed { staker: 11, amount: 50 },
 				Event::Slashed { staker: 101, amount: 7 }
-			] if fraction == slash_fraction_one
+			]
 		));
 
 		let slash_10 = Perbill::from_percent(10);
-		let slash_15 = slash_fraction_two;
+		let slash_15 = Perbill::from_percent(15);
 		let initial_slash = slash_10 * nominated_value;
 
 		let total_slash = slash_15 * nominated_value;
@@ -3282,48 +3395,67 @@ fn remove_deferred() {
 
 #[test]
 fn remove_multi_deferred() {
-	ExtBuilder::default()
-		.slash_defer_duration(2)
-		.validator_count(4)
-		.set_status(41, StakerStatus::Validator)
-		.set_status(51, StakerStatus::Validator)
-		.build_and_execute(|| {
-			mock::start_active_era(1);
+	ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| {
+		mock::start_active_era(1);
 
-			assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
-			assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
+		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
+		let exposure = Staking::eras_stakers(active_era(), &11);
+		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
-			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true);
+		on_offence_now(
+			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
+			&[Perbill::from_percent(10)],
+		);
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true);
+		on_offence_now(
+			&[OffenceDetails {
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
+				reporters: vec![],
+			}],
+			&[Perbill::from_percent(10)],
+		);
 
-			on_offence_now(&[offence_from(41, None)], &[Perbill::from_percent(25)], true);
+		on_offence_now(
+			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
+			&[Perbill::from_percent(25)],
+		);
 
-			on_offence_now(&[offence_from(51, None)], &[Perbill::from_percent(25)], true);
+		on_offence_now(
+			&[OffenceDetails { offender: (42, exposure.clone()), reporters: vec![] }],
+			&[Perbill::from_percent(25)],
+		);
 
-			// there are 5 slashes to be applied in era 3.
-			assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&3).count(), 5);
+		on_offence_now(
+			&[OffenceDetails { offender: (69, exposure.clone()), reporters: vec![] }],
+			&[Perbill::from_percent(25)],
+		);
 
-			// lets cancel 3 of them.
-			assert_ok!(Staking::cancel_deferred_slash(
-				RuntimeOrigin::root(),
-				3,
-				vec![
-					(11, Perbill::from_percent(10), 0),
-					(11, Perbill::from_percent(25), 0),
-					(51, Perbill::from_percent(25), 0),
-				]
-			));
+		assert_eq!(UnappliedSlashes::<Test>::get(&4).len(), 5);
 
-			let slashes = UnappliedSlashes::<Test>::iter_prefix(&3).collect::<Vec<_>>();
-			assert_eq!(slashes.len(), 2);
-			// the first item in the remaining slashes belongs to validator 41.
-			assert_eq!(slashes[0].0, (41, Perbill::from_percent(25), 0));
-			// the second and last item in the remaining slashes belongs to validator 21.
-			assert_eq!(slashes[1].0, (21, Perbill::from_percent(10), 0));
-		})
+		// fails if list is not sorted
+		assert_noop!(
+			Staking::cancel_deferred_slash(RuntimeOrigin::root(), 1, vec![2, 0, 4]),
+			Error::<Test>::NotSortedAndUnique
+		);
+		// fails if list is not unique
+		assert_noop!(
+			Staking::cancel_deferred_slash(RuntimeOrigin::root(), 1, vec![0, 2, 2]),
+			Error::<Test>::NotSortedAndUnique
+		);
+		// fails if bad index
+		assert_noop!(
+			Staking::cancel_deferred_slash(RuntimeOrigin::root(), 1, vec![1, 2, 3, 4, 5]),
+			Error::<Test>::InvalidSlashIndex
+		);
+
+		assert_ok!(Staking::cancel_deferred_slash(RuntimeOrigin::root(), 4, vec![0, 2, 4]));
+
+		let slashes = UnappliedSlashes::<Test>::get(&4);
+		assert_eq!(slashes.len(), 2);
+		assert_eq!(slashes[0].validator, 21);
+		assert_eq!(slashes[1].validator, 42);
+	})
 }
 
 #[test]
@@ -3352,7 +3484,10 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid
 			assert_eq!(exposure_11.total, 1000 + 125);
 			assert_eq!(exposure_21.total, 1000 + 375);
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(10)],
+			);
 
 			assert_eq!(
 				staking_events_since_last_call(),
@@ -3360,12 +3495,11 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid
 					Event::PagedElectionProceeded { page: 0, result: Ok(7) },
 					Event::StakersElected,
 					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 11,
 						fraction: Perbill::from_percent(10),
-						offence_era: 1
+						slash_era: 1
 					},
-					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 },
 					Event::Slashed { staker: 11, amount: 100 },
 					Event::Slashed { staker: 101, amount: 12 },
 				]
@@ -3412,14 +3546,23 @@ fn non_slashable_offence_disables_validator() {
 			mock::start_active_era(1);
 			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
 
+			let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21);
+
 			// offence with no slash associated
-			on_offence_now(&[offence_from(11, None)], &[Perbill::zero()], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::zero()],
+			);
 
 			// it does NOT affect the nominator.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
 
 			// offence that slashes 25% of the bond
-			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(25)], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(25)],
+			);
 
 			// it DOES NOT affect the nominator.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3430,16 +3573,18 @@ fn non_slashable_offence_disables_validator() {
 					Event::PagedElectionProceeded { page: 0, result: Ok(7) },
 					Event::StakersElected,
 					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 11,
 						fraction: Perbill::from_percent(0),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 11 },
 					Event::OffenceReported {
 						validator: 21,
 						fraction: Perbill::from_percent(25),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 21 },
 					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 },
 					Event::Slashed { staker: 21, amount: 250 },
 					Event::Slashed { staker: 101, amount: 94 }
@@ -3472,11 +3617,18 @@ fn slashing_independent_of_disabling_validator() {
 			mock::start_active_era(1);
 			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51]);
 
+			let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21);
+
 			let now = ActiveEra::<Test>::get().unwrap().index;
 
 			// --- Disable without a slash ---
 			// offence with no slash associated
-			on_offence_in_era(&[offence_from(11, None)], &[Perbill::zero()], now, true);
+			on_offence_in_era(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::zero()],
+				now,
+			);
 
 			// nomination remains untouched.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3486,10 +3638,18 @@ fn slashing_independent_of_disabling_validator() {
 
 			// --- Slash without disabling ---
 			// offence that slashes 50% of the bond (setup for next slash)
-			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(50)], now, true);
+			on_offence_in_era(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(50)],
+				now,
+			);
 
 			// offence that slashes 25% of the bond but does not disable
-			on_offence_in_era(&[offence_from(21, None)], &[Perbill::from_percent(25)], now, true);
+			on_offence_in_era(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(25)],
+				now,
+			);
 
 			// nomination remains untouched.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3504,25 +3664,24 @@ fn slashing_independent_of_disabling_validator() {
 					Event::PagedElectionProceeded { page: 0, result: Ok(5) },
 					Event::StakersElected,
 					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 11,
 						fraction: Perbill::from_percent(0),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 11 },
 					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(50),
-						offence_era: 1
+						slash_era: 1
 					},
-					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 },
 					Event::Slashed { staker: 11, amount: 500 },
 					Event::Slashed { staker: 101, amount: 62 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 21,
 						fraction: Perbill::from_percent(25),
-						offence_era: 1
+						slash_era: 1
 					},
-					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 },
 					Event::Slashed { staker: 21, amount: 250 },
 					Event::Slashed { staker: 101, amount: 94 }
 				]
@@ -3558,14 +3717,25 @@ fn offence_threshold_doesnt_plan_new_era() {
 
 			// we have 4 validators and an offending validator threshold of 1/3,
 			// even if the third validator commits an offence a new era should not be forced
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true);
+
+			let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21);
+			let exposure_31 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &31);
+
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(50)],
+			);
 
 			// 11 should be disabled because the byzantine threshold is 1
 			assert!(is_disabled(11));
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
-			on_offence_now(&[offence_from(21, None)], &[Perbill::zero()], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::zero()],
+			);
 
 			// 21 should not be disabled because the number of disabled validators will be above the
 			// byzantine threshold
@@ -3573,7 +3743,10 @@ fn offence_threshold_doesnt_plan_new_era() {
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
-			on_offence_now(&[offence_from(31, None)], &[Perbill::zero()], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }],
+				&[Perbill::zero()],
+			);
 
 			// same for 31
 			assert!(!is_disabled(31));
@@ -3595,7 +3768,13 @@ fn disabled_validators_are_kept_disabled_for_whole_era() {
 			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
 			assert_eq!(<Test as Config>::SessionsPerEra::get(), 3);
 
-			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(25)], true);
+			let exposure_11 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(ActiveEra::<Test>::get().unwrap().index, &21);
+
+			on_offence_now(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(25)],
+			);
 
 			// nominations are not updated.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3609,7 +3788,10 @@ fn disabled_validators_are_kept_disabled_for_whole_era() {
 			assert!(is_disabled(21));
 
 			// validator 11 commits an offence
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(25)],
+			);
 
 			// nominations are not updated.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3725,9 +3907,14 @@ fn zero_slash_keeps_nominators() {
 			mock::start_active_era(1);
 
 			assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
+
+			let exposure = Staking::eras_stakers(active_era(), &11);
 			assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(0)],
+			);
 
 			assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 			assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
@@ -4720,7 +4907,6 @@ fn bond_during_era_does_not_populate_legacy_claimed_rewards() {
 }
 
 #[test]
-#[ignore]
 fn offences_weight_calculated_correctly() {
 	ExtBuilder::default().nominate(true).build_and_execute(|| {
 		// On offence with zero offenders: 4 Reads, 1 Write
@@ -4743,7 +4929,7 @@ fn offences_weight_calculated_correctly() {
 			>,
 		> = (1..10)
 			.map(|i| OffenceDetails {
-				offender: (i, ()),
+				offender: (i, Staking::eras_stakers(active_era(), &i)),
 				reporters: vec![],
 			})
 			.collect();
@@ -4757,7 +4943,10 @@ fn offences_weight_calculated_correctly() {
 		);
 
 		// On Offence with one offenders, Applied
-		let one_offender = [offence_from(11, Some(1))];
+		let one_offender = [OffenceDetails {
+			offender: (11, Staking::eras_stakers(active_era(), &11)),
+			reporters: vec![1],
+		}];
 
 		let n = 1; // Number of offenders
 		let rw = 3 + 3 * n; // rw reads and writes
@@ -6850,7 +7039,13 @@ mod staking_interface {
 	#[test]
 	fn do_withdraw_unbonded_with_wrong_slash_spans_works_as_expected() {
 		ExtBuilder::default().build_and_execute(|| {
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true);
+			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));
 
@@ -7134,7 +7329,13 @@ mod staking_unchecked {
 				let exposed_nominator = initial_exposure.others.first().unwrap().value;
 
 				// 11 goes offline
-				on_offence_now(&[offence_from(11, None)], &[slash_percent], true);
+				on_offence_now(
+					&[OffenceDetails {
+						offender: (11, initial_exposure.clone()),
+						reporters: vec![],
+					}],
+					&[slash_percent],
+				);
 
 				let slash_amount = slash_percent * exposed_stake;
 				let validator_share =
@@ -7200,7 +7401,13 @@ mod staking_unchecked {
 				let nominator_stake = Staking::ledger(101.into()).unwrap().total;
 
 				// 11 goes offline
-				on_offence_now(&[offence_from(11, None)], &[slash_percent], true);
+				on_offence_now(
+					&[OffenceDetails {
+						offender: (11, initial_exposure.clone()),
+						reporters: vec![],
+					}],
+					&[slash_percent],
+				);
 
 				// both stakes must have been decreased to 0.
 				assert_eq!(Staking::ledger(101.into()).unwrap().active, 0);
@@ -8340,9 +8547,19 @@ fn reenable_lower_offenders_mock() {
 			mock::start_active_era(1);
 			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
 
+			let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
+			let exposure_31 = Staking::eras_stakers(Staking::active_era().unwrap().index, &31);
+
 			// offence with a low slash
-			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
-			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(20)], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(10)],
+			);
+			on_offence_now(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(20)],
+			);
 
 			// it does NOT affect the nominator.
 			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
@@ -8352,7 +8569,10 @@ fn reenable_lower_offenders_mock() {
 			assert!(is_disabled(21));
 
 			// offence with a higher slash
-			on_offence_now(&[offence_from(31, None)], &[Perbill::from_percent(50)], true);
+			on_offence_now(
+				&[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(50)],
+			);
 
 			// First offender is no longer disabled
 			assert!(!is_disabled(11));
@@ -8367,27 +8587,31 @@ fn reenable_lower_offenders_mock() {
 					Event::PagedElectionProceeded { page: 0, result: Ok(7) },
 					Event::StakersElected,
 					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 11,
 						fraction: Perbill::from_percent(10),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 11 },
 					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 },
 					Event::Slashed { staker: 11, amount: 100 },
 					Event::Slashed { staker: 101, amount: 12 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 21,
 						fraction: Perbill::from_percent(20),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 21 },
 					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 },
 					Event::Slashed { staker: 21, amount: 200 },
 					Event::Slashed { staker: 101, amount: 75 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 31,
 						fraction: Perbill::from_percent(50),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 31 },
+					Event::ValidatorReenabled { stash: 11 },
 					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 31, page: 0 },
 					Event::Slashed { staker: 31, amount: 250 },
 				]
@@ -8418,17 +8642,33 @@ fn do_not_reenable_higher_offenders_mock() {
 			mock::start_active_era(1);
 			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
 
+			let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
+			let exposure_31 = Staking::eras_stakers(Staking::active_era().unwrap().index, &31);
+
 			// offence with a major slash
 			on_offence_now(
-				&[offence_from(11, None), offence_from(21, None), offence_from(31, None)],
-				&[Perbill::from_percent(50), Perbill::from_percent(50), Perbill::from_percent(10)],
-				true,
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(50)],
+			);
+			on_offence_now(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(50)],
 			);
 
 			// both validators should be disabled
 			assert!(is_disabled(11));
 			assert!(is_disabled(21));
 
+			// offence with a minor slash
+			on_offence_now(
+				&[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(10)],
+			);
+
+			// First and second offenders are still disabled
+			assert!(is_disabled(11));
+			assert!(is_disabled(21));
 			// New offender is not disabled as limit is reached and his prio is lower
 			assert!(!is_disabled(31));
 
@@ -8438,22 +8678,23 @@ fn do_not_reenable_higher_offenders_mock() {
 					Event::PagedElectionProceeded { page: 0, result: Ok(7) },
 					Event::StakersElected,
 					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-					Event::OffenceReported {
+					Event::SlashReported {
 						validator: 11,
 						fraction: Perbill::from_percent(50),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 11 },
 					Event::OffenceReported {
 						validator: 21,
 						fraction: Perbill::from_percent(50),
-						offence_era: 1
+						slash_era: 1
 					},
+					Event::ValidatorDisabled { stash: 21 },
 					Event::OffenceReported {
 						validator: 31,
 						fraction: Perbill::from_percent(10),
-						offence_era: 1
+						slash_era: 1
 					},
-					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 31, page: 0 },
 					Event::Slashed { staker: 31, amount: 50 },
 					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 21, page: 0 },
 					Event::Slashed { staker: 21, amount: 500 },
diff --git a/substrate/frame/staking/src/weights.rs b/substrate/frame/staking/src/weights.rs
index 1ccb534e4c5..d252387e741 100644
--- a/substrate/frame/staking/src/weights.rs
+++ b/substrate/frame/staking/src/weights.rs
@@ -106,7 +106,6 @@ pub trait WeightInfo {
 	fn set_min_commission() -> Weight;
 	fn restore_ledger() -> Weight;
 	fn migrate_currency() -> Weight;
-	fn apply_slash() -> Weight;
 	fn manual_slash() -> Weight;
 }
 
@@ -871,33 +870,7 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
 			.saturating_add(T::DbWeight::get().reads(6_u64))
 			.saturating_add(T::DbWeight::get().writes(2_u64))
 	}
-	/// Storage: `Staking::ActiveEra` (r:1 w:0)
-	/// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::UnappliedSlashes` (r:1 w:1)
-	/// Proof: `Staking::UnappliedSlashes` (`max_values`: None, `max_size`: Some(1694), added: 4169, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::Bonded` (r:33 w:0)
-	/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::Ledger` (r:33 w:33)
-	/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
-	/// Storage: `NominationPools::ReversePoolIdLookup` (r:33 w:0)
-	/// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`)
-	/// Storage: `DelegatedStaking::Agents` (r:33 w:33)
-	/// Proof: `DelegatedStaking::Agents` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`)
-	/// Storage: `System::Account` (r:33 w:33)
-	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::VirtualStakers` (r:33 w:0)
-	/// Proof: `Staking::VirtualStakers` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`)
-	/// Storage: `Balances::Holds` (r:33 w:33)
-	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(427), added: 2902, mode: `MaxEncodedLen`)
-	fn apply_slash() -> Weight {
-		// Proof Size summary in bytes:
-		//  Measured:  `14542`
-		//  Estimated: `118668`
-		// Minimum execution time: 1_628_472_000 picoseconds.
-		Weight::from_parts(1_647_487_000, 118668)
-			.saturating_add(T::DbWeight::get().reads(233_u64))
-			.saturating_add(T::DbWeight::get().writes(133_u64))
-	}
+
 	/// Storage: `Staking::CurrentEra` (r:1 w:0)
 	/// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
 	/// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0)
@@ -1689,33 +1662,6 @@ impl WeightInfo for () {
 			.saturating_add(RocksDbWeight::get().reads(6_u64))
 			.saturating_add(RocksDbWeight::get().writes(2_u64))
 	}
-	/// Storage: `Staking::ActiveEra` (r:1 w:0)
-	/// Proof: `Staking::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::UnappliedSlashes` (r:1 w:1)
-	/// Proof: `Staking::UnappliedSlashes` (`max_values`: None, `max_size`: Some(1694), added: 4169, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::Bonded` (r:33 w:0)
-	/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::Ledger` (r:33 w:33)
-	/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`)
-	/// Storage: `NominationPools::ReversePoolIdLookup` (r:33 w:0)
-	/// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`)
-	/// Storage: `DelegatedStaking::Agents` (r:33 w:33)
-	/// Proof: `DelegatedStaking::Agents` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`)
-	/// Storage: `System::Account` (r:33 w:33)
-	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
-	/// Storage: `Staking::VirtualStakers` (r:33 w:0)
-	/// Proof: `Staking::VirtualStakers` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`)
-	/// Storage: `Balances::Holds` (r:33 w:33)
-	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(427), added: 2902, mode: `MaxEncodedLen`)
-	fn apply_slash() -> Weight {
-		// Proof Size summary in bytes:
-		//  Measured:  `14542`
-		//  Estimated: `118668`
-		// Minimum execution time: 1_628_472_000 picoseconds.
-		Weight::from_parts(1_647_487_000, 118668)
-			.saturating_add(RocksDbWeight::get().reads(233_u64))
-			.saturating_add(RocksDbWeight::get().writes(133_u64))
-	}
 	/// Storage: `Staking::CurrentEra` (r:1 w:0)
 	/// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
 	/// Storage: `Staking::ErasStartSessionIndex` (r:1 w:0)
-- 
GitLab