From dda2cb5969985ccbf67581e18eb7c579849e27bb Mon Sep 17 00:00:00 2001
From: Ankan <10196091+Ank4n@users.noreply.github.com>
Date: Tue, 18 Feb 2025 00:07:09 +0100
Subject: [PATCH] [Staking] Bounded Slashing: Paginated Offence Processing &
 Slash Application (#7424)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

closes https://github.com/paritytech/polkadot-sdk/issues/3610.

helps https://github.com/paritytech/polkadot-sdk/issues/6344, but need
to migrate storage `Offences::Reports` before we can remove exposure
dependency in RC pallets.

replaces https://github.com/paritytech/polkadot-sdk/issues/6788.

## Context
Slashing in staking is unbounded currently, which is a major blocker
until staking can move to a parachain (AH).

### Current Slashing Process (Unbounded)

1. **Offence Reported**
- Offences include multiple validators, each with potentially large
exposure pages.
- Slashes are **computed immediately** and scheduled for application
after **28 eras**.

2. **Slash Applied**
- All unapplied slashes are executed in **one block** at the start of
the **28th era**. This is an **unbounded operation**.


### Proposed Slashing Process (Bounded)

1. **Offence Queueing**
   - Offences are **queued** after basic sanity checks.

2. **Paged Offence Processing (Computing Slash)**
   - Slashes are **computed one validator exposure page at a time**.
   - **Unapplied slashes** are stored in a **double map**:
     - **Key 1 (k1):** `EraIndex`
- **Key 2 (k2):** `(Validator, SlashFraction, PageIndex)` — a unique
identifier for each slash page

3. **Paged Slash Application**
- Slashes are **applied one page at a time** across multiple blocks.
- Slash application starts at the **27th era** (one era earlier than
before) to ensure all slashes are applied **before stakers can unbond**
(which starts from era 28 onwards).

---

## Worst-Case Block Calculation for Slash Application

### Polkadot:
- **1 era = 24 hours**, **1 block = 6s** → **14,400 blocks/era**
- On parachains (**12s blocks**) → **7,200 blocks/era**

### Kusama:
- **1 era = 6 hours**, **1 block = 6s** → **3,600 blocks/era**
- On parachains (**12s blocks**) → **1,800 blocks/era**

### Worst-Case Assumptions:
- **Total stakers:** 40,000 nominators, 1000 validators. (Polkadot
currently has ~23k nominators and 500 validators)
- **Max slashed:** 50% so 20k nominators, 250 validators.
- **Page size:** Validators with multiple page: (512 + 1)/2 = 256 ,
Validators with single page: 1

### Calculation:
There might be a more accurate way to calculate this worst-case number,
and this estimate could be significantly higher than necessary, but it
shouldn’t exceed this value.

Blocks needed: 250 + 20k/256 = ~330 blocks.

##  *Potential Improvement:*
- Consider adding an **Offchain Worker (OCW)** task to further optimize
slash application in future updates.
- Dynamically batch unapplied slashes based on number of nominators in
the page, or process until reserved weight limit is exhausted.

----
## Summary of Changes

### Storage
- **New:**
  - `OffenceQueue` *(StorageDoubleMap)*
    - **K1:** Era
    - **K2:** Offending validator account
    - **V:** `OffenceRecord`
  - `OffenceQueueEras` *(StorageValue)*
    - **V:** `BoundedVec<EraIndex, BoundingDuration>`
  - `ProcessingOffence` *(StorageValue)*
    - **V:** `(Era, offending validator account, OffenceRecord)`

- **Changed:**
  - `UnappliedSlashes`:
    - **Old:** `StorageMap<K -> Era, V -> Vec<UnappliedSlash>>`
- **New:** `StorageDoubleMap<K1 -> Era, K2 -> (validator_acc, perbill,
page_index), V -> UnappliedSlash>`

### Events
- **New:**
  - `SlashComputed { offence_era, slash_era, offender, page }`
  - `SlashCancelled { slash_era, slash_key, payout }`

### Error
- **Changed:**
  - `InvalidSlashIndex` → Renamed to `InvalidSlashRecord`
- **Removed:**
  - `NotSortedAndUnique`
- **Added:**
  - `EraNotStarted`

### Call
- **Changed:**
  - `cancel_deferred_slash(era, slash_indices: Vec<u32>)`
    → Now takes `Vec<(validator_acc, slash_fraction, page_index)>`
- **New:**
- `apply_slash(slash_era, slash_key: (validator_acc, slash_fraction,
page_index))`

### Runtime Config
- `FullIdentification` is now set to a unit type (`()`) / null identity,
replacing the previous exposure type for all runtimes using
`pallet_session::historical`.

## TODO
- [x] Fixed broken `CancelDeferredSlashes`.
- [x] Ensure on_offence called only with validator account for
identification everywhere.
- [ ] Ensure we never need to read full exposure.
- [x] Tests for multi block processing and application of slash.
- [x] Migrate UnappliedSlashes
- [x] Bench (crude, needs proper bench as followup)
  - [x] on_offence()
  - [x] process_offence()
  - [x] apply_slash()


## Followups (tracker
[link](https://github.com/paritytech/polkadot-sdk/issues/7596))
- [ ] OCW task to process offence + apply slashes.
- [ ] Minimum time for governance to cancel deferred slash.
- [ ] Allow root or staking admin to add a custom slash.
- [ ] Test HistoricalSession proof works fine with eras before removing
exposure as full identity.
- [ ] Properly bench offence processing and slashing.
- [ ] Handle Offences::Reports migration when removing validator
exposure as identity.

---------

Co-authored-by: Gonçalo Pestana <g6pestana@gmail.com>
Co-authored-by: command-bot <>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Giuseppe Re <giuseppe.re@parity.io>
Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 polkadot/runtime/test-runtime/src/lib.rs      |    4 +-
 polkadot/runtime/westend/src/lib.rs           |    1 +
 .../westend/src/weights/pallet_staking.rs     |    4 +
 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     |  343 ++----
 substrate/frame/staking/src/mock.rs           |   42 +-
 substrate/frame/staking/src/pallet/impls.rs   |  294 +++--
 substrate/frame/staking/src/pallet/mod.rs     |  200 ++-
 substrate/frame/staking/src/slashing.rs       |  409 +++++--
 substrate/frame/staking/src/tests.rs          | 1070 ++++++++++-------
 substrate/frame/staking/src/weights.rs        |    9 +
 24 files changed, 1605 insertions(+), 1061 deletions(-)
 create 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 c0985873532..1656bbac525 100644
--- a/polkadot/runtime/test-runtime/src/lib.rs
+++ b/polkadot/runtime/test-runtime/src/lib.rs
@@ -322,8 +322,8 @@ impl pallet_session::Config for Runtime {
 }
 
 impl pallet_session::historical::Config for Runtime {
-	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 pallet_staking_reward_curve::build! {
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index 5d7a8f51625..8fc1a3af01a 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -1871,6 +1871,7 @@ 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>,
 		// permanent
 		pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>,
 	);
diff --git a/polkadot/runtime/westend/src/weights/pallet_staking.rs b/polkadot/runtime/westend/src/weights/pallet_staking.rs
index f0491a1daf6..add70e85fb4 100644
--- a/polkadot/runtime/westend/src/weights/pallet_staking.rs
+++ b/polkadot/runtime/westend/src/weights/pallet_staking.rs
@@ -805,4 +805,8 @@ 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))
 	}
+	fn apply_slash() -> Weight {
+		// TODO CI-FAIL: run CI bench bot
+		Weight::zero()
+	}
 }
diff --git a/prdoc/pr_7424.prdoc b/prdoc/pr_7424.prdoc
new file mode 100644
index 00000000000..e177f41371b
--- /dev/null
+++ b/prdoc/pr_7424.prdoc
@@ -0,0 +1,37 @@
+# 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 bebf48618ba..9adebe8502c 100644
--- a/substrate/bin/node/runtime/src/lib.rs
+++ b/substrate/bin/node/runtime/src/lib.rs
@@ -676,8 +676,6 @@ 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
@@ -712,61 +710,43 @@ pub mod staking_playground {
 		}
 	}
 
-	impl pallet_session::historical::SessionManager<AccountId, Exposure<AccountId, Balance>>
-		for AliceAsOnlyValidator
-	{
+	impl pallet_session::historical::SessionManager<AccountId, ()> for AliceAsOnlyValidator {
 		fn end_session(end_index: sp_staking::SessionIndex) {
-			<Staking as pallet_session::historical::SessionManager<
-				AccountId,
-				Exposure<AccountId, Balance>,
-			>>::end_session(end_index)
+			<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::end_session(
+				end_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)
+		fn new_session(new_index: sp_staking::SessionIndex) -> Option<Vec<(AccountId, ())>> {
+			<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::new_session(
+				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![],
-					},
-				)]
+				vec![(sp_keyring::Sr25519Keyring::AliceStash.to_account_id().into(), ())]
 			})
 		}
 
 		fn new_session_genesis(
 			new_index: sp_staking::SessionIndex,
-		) -> Option<Vec<(AccountId, Exposure<AccountId, Balance>)>> {
+		) -> Option<Vec<(AccountId, ())>> {
 			<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,
-				Exposure<AccountId, Balance>,
-			>>::start_session(start_index)
+			<Staking as pallet_session::historical::SessionManager<AccountId, ()>>::start_session(
+				start_index,
+			)
 		}
 	}
 }
@@ -790,8 +770,8 @@ impl pallet_session::Config for Runtime {
 }
 
 impl pallet_session::historical::Config for Runtime {
-	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 pallet_staking_reward_curve::build! {
diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs
index 1e4f51d5143..df39cfd7f06 100644
--- a/substrate/frame/babe/src/mock.rs
+++ b/substrate/frame/babe/src/mock.rs
@@ -104,8 +104,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = pallet_staking::Exposure<u64, u128>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 impl pallet_authorship::Config for Test {
diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs
index 2f90edf3c35..5d5b16b939f 100644
--- a/substrate/frame/beefy/src/mock.rs
+++ b/substrate/frame/beefy/src/mock.rs
@@ -188,8 +188,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = pallet_staking::Exposure<u64, u128>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 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 8c8de865600..0b8380f7a1c 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
@@ -145,8 +145,8 @@ impl pallet_session::Config for Runtime {
 	type WeightInfo = ();
 }
 impl pallet_session::historical::Config for Runtime {
-	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Runtime>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 frame_election_provider_support::generate_solution_type!(
@@ -908,10 +908,7 @@ 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, Staking::eras_stakers(active_era(), who)),
-			reporters: vec![],
-		}],
+		&[OffenceDetails { offender: (*who, ()), reporters: vec![] }],
 		&[Perbill::from_percent(10)],
 	);
 }
diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs
index a14bdc9d73b..5d224f674ec 100644
--- a/substrate/frame/grandpa/src/mock.rs
+++ b/substrate/frame/grandpa/src/mock.rs
@@ -108,8 +108,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = pallet_staking::Exposure<u64, u128>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 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 3d3cd470bc2..fa4349d1d94 100644
--- a/substrate/frame/offences/benchmarking/src/inner.rs
+++ b/substrate/frame/offences/benchmarking/src/inner.rs
@@ -170,6 +170,13 @@ 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
@@ -182,10 +189,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
+	// (n nominators + one validator) * slashed + Slash Reported + Slash Computed
 	assert_eq!(
 		System::<T>::read_events_for_pallet::<pallet_staking::Event<T>>().len(),
-		1 * (offender_count + 1) as usize + 1
+		1 * (offender_count + 1) as usize + 2
 	);
 	// offence
 	assert_eq!(System::<T>::read_events_for_pallet::<pallet_offences::Event>().len(), 1);
@@ -232,6 +239,8 @@ mod benchmarks {
 
 		#[cfg(test)]
 		{
+			// slashes applied at the next block.
+			run_staking_next_block::<T>();
 			assert_all_slashes_applied::<T>(n as usize);
 		}
 
@@ -266,6 +275,8 @@ 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 46a4e18c5e8..28c6ed1bef7 100644
--- a/substrate/frame/offences/benchmarking/src/mock.rs
+++ b/substrate/frame/offences/benchmarking/src/mock.rs
@@ -33,7 +33,6 @@ use sp_runtime::{
 };
 
 type AccountId = u64;
-type Balance = u64;
 
 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
 impl frame_system::Config for Test {
@@ -54,8 +53,8 @@ impl pallet_timestamp::Config for Test {
 	type WeightInfo = ();
 }
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 sp_runtime::impl_opaque_keys! {
diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs
index fd6ffc55e40..8e91c4ecfd1 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::{BalanceOf, Exposure, ExposureOf, Pallet as Staking};
+use pallet_staking::Pallet as Staking;
 use sp_runtime::Perbill;
 use sp_staking::offence::OnOffenceHandler;
 
@@ -49,11 +49,8 @@ pub mod pallet {
 		+ pallet_staking::Config
 		+ pallet_session::Config<ValidatorId = <Self as frame_system::Config>::AccountId>
 		+ pallet_session::historical::Config<
-			FullIdentification = Exposure<
-				<Self as frame_system::Config>::AccountId,
-				BalanceOf<Self>,
-			>,
-			FullIdentificationOf = ExposureOf<Self>,
+			FullIdentification = (),
+			FullIdentificationOf = pallet_staking::NullIdentity,
 		>
 	{
 		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
@@ -106,15 +103,11 @@ 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(), Staking::<T>::eras_stakers(now, &o)),
+					offender: (o.clone(), ()),
 					reporters: Default::default(),
 				})
 				.collect())
@@ -124,7 +117,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();
 
-			<pallet_staking::Pallet<T> as OnOffenceHandler<
+			<Staking<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 2303221c881..b8db742f1f8 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::StakerStatus;
+use pallet_staking::{BalanceOf, 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 = pallet_staking::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
+	type FullIdentification = ();
+	type FullIdentificationOf = pallet_staking::NullIdentity;
 }
 
 sp_runtime::impl_opaque_keys! {
@@ -297,6 +297,11 @@ 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 289bb708efb..da6c49895be 100644
--- a/substrate/frame/root-offences/src/tests.rs
+++ b/substrate/frame/root-offences/src/tests.rs
@@ -17,7 +17,10 @@
 
 use super::*;
 use frame_support::{assert_err, assert_ok};
-use mock::{active_era, start_session, ExtBuilder, RootOffences, RuntimeOrigin, System, Test as T};
+use mock::{
+	active_era, advance_blocks, start_session, ExtBuilder, RootOffences, RuntimeOrigin, System,
+	Test as T,
+};
 use pallet_staking::asset;
 
 #[test]
@@ -42,6 +45,10 @@ 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);
 
@@ -66,6 +73,9 @@ 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 b0681f5aa00..a67e2f28258 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 = pallet_staking::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
+	type FullIdentification = ();
+	type FullIdentificationOf = NullIdentity;
 }
 
 sp_runtime::impl_opaque_keys! {
diff --git a/substrate/frame/staking/src/benchmarking.rs b/substrate/frame/staking/src/benchmarking.rs
index 0d084629d66..1978449bb4b 100644
--- a/substrate/frame/staking/src/benchmarking.rs
+++ b/substrate/frame/staking/src/benchmarking.rs
@@ -802,21 +802,33 @@ mod benchmarks {
 
 	#[benchmark]
 	fn cancel_deferred_slash(s: Linear<1, MAX_SLASHES>) {
-		let mut unapplied_slashes = Vec::new();
 		let era = EraIndex::one();
-		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()));
+		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);
 		}
-		UnappliedSlashes::<T>::insert(era, &unapplied_slashes);
 
-		let slash_indices: Vec<u32> = (0..s).collect();
+		let slash_keys: Vec<_> = (0..s)
+			.map(|i| (dummy_account(), Perbill::from_percent(i as u32 % 100), i))
+			.collect();
 
 		#[extrinsic_call]
-		_(RawOrigin::Root, era, slash_indices);
+		_(RawOrigin::Root, era, slash_keys.clone());
 
-		assert_eq!(UnappliedSlashes::<T>::get(&era).len(), (MAX_SLASHES - s) as usize);
+		// Ensure all `s` slashes are removed
+		for key in &slash_keys {
+			assert!(UnappliedSlashes::<T>::get(era, key).is_none());
+		}
 	}
 
 	#[benchmark]
@@ -1137,6 +1149,46 @@ 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(())
+	}
+
 	impl_benchmark_test_suite!(
 		Staking,
 		crate::mock::ExtBuilder::default().has_stakers(true),
diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs
index f97b4ed30b8..0d098cfe7b6 100644
--- a/substrate/frame/staking/src/lib.rs
+++ b/substrate/frame/staking/src/lib.rs
@@ -353,7 +353,7 @@ use frame_support::{
 		ConstU32, Defensive, DefensiveMax, DefensiveSaturating, Get, LockIdentifier,
 	},
 	weights::Weight,
-	BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
+	BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, WeakBoundedVec,
 };
 use scale_info::TypeInfo;
 use sp_runtime::{
@@ -845,31 +845,19 @@ 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, RuntimeDebug, TypeInfo)]
-pub struct UnappliedSlash<AccountId, Balance: HasCompact> {
+#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEqNoBound)]
+#[scale_info(skip_type_params(T))]
+pub struct UnappliedSlash<T: Config> {
 	/// The stash ID of the offending validator.
-	validator: AccountId,
+	validator: T::AccountId,
 	/// The validator's own slash.
-	own: Balance,
+	own: BalanceOf<T>,
 	/// All other slashed stakers and amounts.
-	others: Vec<(AccountId, Balance)>,
+	others: WeakBoundedVec<(T::AccountId, BalanceOf<T>), T::MaxExposurePageSize>,
 	/// Reporters of the offence; bounty payout recipients.
-	reporters: Vec<AccountId>,
+	reporter: Option<T::AccountId>,
 	/// The amount of payout.
-	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(),
-		}
-	}
+	payout: BalanceOf<T>,
 }
 
 /// Something that defines the maximum number of nominations per nominator based on a curve.
@@ -921,10 +909,7 @@ 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<
-		FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
-		FullIdentificationOf = ExposureOf<T>,
-	>,
+	T: pallet_session::historical::Config,
 	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<
@@ -1071,6 +1056,13 @@ 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 08667dd6176..c13c373383e 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,10 +36,6 @@ 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
@@ -60,6 +56,89 @@ impl Default for ObsoleteReleases {
 #[storage_alias]
 type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;
 
+/// Migrates `UnappliedSlashes` to a new storage structure to support paged slashing.
+/// This ensures that slashing can be processed in batches, preventing large storage operations in a
+/// single block.
+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]
+	type OldUnappliedSlashes<T: Config> =
+		StorageMap<Pallet<T>, Twox64Concat, EraIndex, Vec<OldUnappliedSlash<T>>, 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,
+	>;
+}
+
 /// Migrating `DisabledValidators` from `Vec<u32>` to `Vec<(u32, OffenceSeverity)>` to track offense
 /// severity for re-enabling purposes.
 pub mod v16 {
@@ -446,257 +525,3 @@ 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 fdf14976a7d..1efe0542e65 100644
--- a/substrate/frame/staking/src/mock.rs
+++ b/substrate/frame/staking/src/mock.rs
@@ -152,8 +152,8 @@ impl pallet_session::Config for Test {
 }
 
 impl pallet_session::historical::Config for Test {
-	type FullIdentification = crate::Exposure<AccountId, Balance>;
-	type FullIdentificationOf = crate::ExposureOf<Test>;
+	type FullIdentification = ();
+	type FullIdentificationOf = NullIdentity;
 }
 impl pallet_authorship::Config for Test {
 	type FindAuthor = Author11;
@@ -719,6 +719,11 @@ 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();
@@ -821,11 +826,21 @@ 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 {
 			let _ = Staking::on_offence(offenders, slash_fraction, start_session);
+			if advance_processing_blocks {
+				advance_blocks(process_blocks as u64);
+			}
 			return
 		} else if bonded_era > era {
 			break
@@ -838,6 +853,9 @@ 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);
 	}
@@ -849,19 +867,23 @@ 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)
+	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(),
+	}
 }
 
 pub(crate) fn add_slash(who: &AccountId) {
-	on_offence_now(
-		&[OffenceDetails {
-			offender: (*who, Staking::eras_stakers(active_era(), who)),
-			reporters: vec![],
-		}],
-		&[Perbill::from_percent(10)],
-	);
+	on_offence_now(&[offence_from(*who, None)], &[Perbill::from_percent(10)], true);
 }
 
 /// 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 8ca018c7d8b..ea7f0c36326 100644
--- a/substrate/frame/staking/src/pallet/impls.rs
+++ b/substrate/frame/staking/src/pallet/impls.rs
@@ -34,9 +34,7 @@ use frame_support::{
 use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
 use pallet_session::historical;
 use sp_runtime::{
-	traits::{
-		Bounded, CheckedAdd, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero,
-	},
+	traits::{Bounded, CheckedAdd, Convert, SaturatedConversion, Saturating, StaticLookup, Zero},
 	ArithmeticError, DispatchResult, Perbill, Percent,
 };
 use sp_staking::{
@@ -49,15 +47,16 @@ use sp_staking::{
 
 use crate::{
 	asset, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo,
-	BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, ExposureOf, Forcing,
-	IndividualExposure, LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, MaxWinnersPerPageOf,
-	Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface,
-	SnapshotStatus, StakingLedger, ValidatorPrefs, STAKING_ID,
+	BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, 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"))]
@@ -577,8 +576,6 @@ impl<T: Config> Pallet<T> {
 				}
 			}
 		});
-
-		Self::apply_unapplied_slashes(active_era);
 	}
 
 	/// Compute payout for era.
@@ -983,17 +980,19 @@ impl<T: Config> Pallet<T> {
 	}
 
 	/// Apply previously-unapplied slashes on the beginning of a new era, after a delay.
-	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);
+	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);
 		}
 	}
 
@@ -1762,6 +1761,23 @@ 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>
@@ -1779,10 +1795,7 @@ impl<T: Config>
 	for Pallet<T>
 where
 	T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
-	T: pallet_session::historical::Config<
-		FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
-		FullIdentificationOf = ExposureOf<T>,
-	>,
+	T: pallet_session::historical::Config,
 	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<
@@ -1790,124 +1803,190 @@ 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,
-			pallet_session::historical::IdentificationTuple<T>,
-		>],
-		slash_fraction: &[Perbill],
+		offenders: &[OffenceDetails<T::AccountId, historical::IdentificationTuple<T>>],
+		slash_fractions: &[Perbill],
 		slash_session: SessionIndex,
 	) -> Weight {
-		let reward_proportion = SlashRewardFraction::<T>::get();
-		let mut consumed_weight = Weight::from_parts(0, 0);
+		log!(
+			debug,
+			"🦹 on_offence: offenders={:?}, slash_fractions={:?}, slash_session={}",
+			offenders,
+			slash_fractions,
+			slash_session,
+		);
+
+		// todo(ank4n): Needs to be properly benched.
+		let mut consumed_weight = Weight::zero();
 		let mut add_db_reads_writes = |reads, writes| {
 			consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
 		};
 
-		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
-			});
+		// 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 window_start = active_era.saturating_sub(T::BondingDuration::get());
+		add_db_reads_writes(1, 0);
+		let active_era_start_session =
+			ErasStartSessionIndex::<T>::get(active_era.index).unwrap_or(0);
 
 		// 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 slash_era = if slash_session >= active_era_start_session_index {
-			active_era
+		let offence_era = if slash_session >= active_era_start_session {
+			active_era.index
 		} else {
-			let eras = BondedEras::<T>::get();
 			add_db_reads_writes(1, 0);
-
-			// 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,
+			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()
+				},
 			}
 		};
 
-		add_db_reads_writes(1, 1);
-
-		let slash_defer_duration = T::SlashDeferDuration::get();
-
-		let invulnerables = Invulnerables::<T>::get();
 		add_db_reads_writes(1, 0);
+		let invulnerables = Invulnerables::<T>::get();
 
-		for (details, slash_fraction) in offenders.iter().zip(slash_fraction) {
-			let (stash, exposure) = &details.offender;
-
+		for (details, slash_fraction) in offenders.iter().zip(slash_fractions) {
+			let (validator, _) = &details.offender;
 			// Skip if the validator is invulnerable.
-			if invulnerables.contains(stash) {
+			if invulnerables.contains(&validator) {
+				log!(debug, "🦹 on_offence: {:?} is invulnerable; ignoring offence", validator);
 				continue
 			}
 
-			Self::deposit_event(Event::<T>::SlashReported {
-				validator: stash.clone(),
+			add_db_reads_writes(1, 0);
+			let Some(exposure_overview) = <ErasStakersOverview<T>>::get(&offence_era, validator)
+			else {
+				// defensive: this implies offence is for a discarded era, and should already be
+				// filtered out.
+				log!(
+					warn,
+					"🦹 on_offence: no exposure found for {:?} in era {}; ignoring offence",
+					validator,
+					offence_era
+				);
+				continue;
+			};
+
+			Self::deposit_event(Event::<T>::OffenceReported {
+				validator: validator.clone(),
 				fraction: *slash_fraction,
-				slash_era,
+				offence_era,
 			});
 
-			let unapplied = slashing::compute_slash::<T>(slashing::SlashParams {
-				stash,
-				slash: *slash_fraction,
-				exposure,
-				slash_era,
-				window_start,
-				now: active_era,
-				reward_proportion,
-			});
+			// add offending validator to the set of offenders.
+			add_db_reads_writes(1, 1);
+			slashing::add_offending_validator::<T>(validator, *slash_fraction, offence_era);
 
-			if let Some(mut unapplied) = unapplied {
-				let nominators_len = unapplied.others.len() as u64;
-				let reporters_len = details.reporters.len() as u64;
+			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
+						},
+					);
+
+					// update the slash fraction in the `ValidatorSlashInEra` storage.
+					ValidatorSlashInEra::<T>::insert(
+						offence_era,
+						validator,
+						(slash_fraction, exposure_overview.own),
+					);
 
-				{
-					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,
-						"deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
+						"🦹 updated slash for {:?}: {:?} (prior: {:?})",
+						validator,
 						slash_fraction,
-						slash_era,
-						active_era,
-						slash_era + slash_defer_duration + 1,
+						prior_slash_fraction,
 					);
-					UnappliedSlashes::<T>::mutate(
-						slash_era.saturating_add(slash_defer_duration).saturating_add(One::one()),
-						move |for_later| for_later.push(unapplied),
+				} else {
+					log!(
+						debug,
+						"🦹 ignored slash for {:?}: {:?} (existing prior is larger: {:?})",
+						validator,
+						slash_fraction,
+						prior_slash_fraction,
 					);
-					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 {
-				add_db_reads_writes(4 /* fetch_spans */, 5 /* kick_out_if_recent */)
+				log!(
+					debug,
+					"🦹 ignored slash for {:?}: {:?} (already slashed in era with prior: {:?})",
+					validator,
+					slash_fraction,
+					prior_slash_fraction,
+				);
 			}
 		}
 
@@ -2345,6 +2424,7 @@ 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 7d22df148de..52f95a6c075 100644
--- a/substrate/frame/staking/src/pallet/mod.rs
+++ b/substrate/frame/staking/src/pallet/mod.rs
@@ -78,7 +78,7 @@ pub mod pallet {
 	use frame_election_provider_support::{ElectionDataProvider, PageIndex};
 
 	/// The in-code storage version.
-	const STORAGE_VERSION: StorageVersion = StorageVersion::new(16);
+	const STORAGE_VERSION: StorageVersion = StorageVersion::new(17);
 
 	#[pallet::pallet]
 	#[pallet::storage_version(STORAGE_VERSION)]
@@ -645,15 +645,67 @@ 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]
-	#[pallet::unbounded]
-	pub type UnappliedSlashes<T: Config> = StorageMap<
+	pub type UnappliedSlashes<T: Config> = StorageDoubleMap<
 		_,
 		Twox64Concat,
 		EraIndex,
-		Vec<UnappliedSlash<T::AccountId, BalanceOf<T>>>,
-		ValueQuery,
+		Twox64Concat,
+		// Unique key for unapplied slashes: (validator, slash fraction, page index).
+		(T::AccountId, Perbill, u32),
+		UnappliedSlash<T>,
+		OptionQuery,
 	>;
 
 	/// A mapping from still-bonded eras to the first session index of that era.
@@ -936,13 +988,6 @@ 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 {
@@ -1034,6 +1079,26 @@ 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]
@@ -1051,8 +1116,8 @@ pub mod pallet {
 		EmptyTargets,
 		/// Duplicate index.
 		DuplicateIndex,
-		/// Slash record index out of bounds.
-		InvalidSlashIndex,
+		/// Slash record not found.
+		InvalidSlashRecord,
 		/// 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.
@@ -1067,8 +1132,6 @@ 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.
@@ -1109,6 +1172,8 @@ pub mod pallet {
 		CannotReapStash,
 		/// The stake of this account is already migrated to `Fungible` holds.
 		AlreadyMigrated,
+		/// Era not yet started.
+		EraNotStarted,
 	}
 
 	#[pallet::hooks]
@@ -1117,6 +1182,21 @@ 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.
@@ -1144,7 +1224,9 @@ pub mod pallet {
 				}
 			};
 
-			T::WeightInfo::on_initialize_noop().saturating_add(inner_weight)
+			consumed_weight.saturating_accrue(inner_weight);
+
+			consumed_weight
 		}
 
 		fn on_finalize(_n: BlockNumberFor<T>) {
@@ -1907,33 +1989,35 @@ pub mod pallet {
 			Ok(())
 		}
 
-		/// Cancel enactment of a deferred slash.
+		/// Cancels scheduled slashes for a given era before they are applied.
 		///
-		/// Can be called by the `T::AdminOrigin`.
+		/// This function allows `T::AdminOrigin` to selectively remove pending slashes from
+		/// the `UnappliedSlashes` storage, preventing their enactment.
 		///
-		/// Parameters: era and indices of the slashes for that era to kill.
+		/// ## 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)`.
 		#[pallet::call_index(17)]
-		#[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_indices.len() as u32))]
+		#[pallet::weight(T::WeightInfo::cancel_deferred_slash(slash_keys.len() as u32))]
 		pub fn cancel_deferred_slash(
 			origin: OriginFor<T>,
 			era: EraIndex,
-			slash_indices: Vec<u32>,
+			slash_keys: Vec<(T::AccountId, Perbill, u32)>,
 		) -> DispatchResult {
 			T::AdminOrigin::ensure_origin(origin)?;
-
-			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);
+			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,
+					});
+				});
+			});
 			Ok(())
 		}
 
@@ -2494,10 +2578,44 @@ pub mod pallet {
 			// Refund the transaction fee if successful.
 			Ok(Pays::No.into())
 		}
-	}
-}
 
-/// Check that list is sorted and has no duplicates.
-fn is_sorted_and_unique(list: &[u32]) -> bool {
-	list.windows(2).all(|w| w[0] < w[1])
+		/// 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())
+		}
+	}
 }
diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs
index 98a6424fe7a..eaa7fb080b1 100644
--- a/substrate/frame/staking/src/slashing.rs
+++ b/substrate/frame/staking/src/slashing.rs
@@ -50,20 +50,21 @@
 //! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html>
 
 use crate::{
-	asset, BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure,
-	NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash,
-	UnappliedSlash, ValidatorSlashInEra,
+	asset, log, BalanceOf, Config, DisabledValidators, DisablingStrategy, EraInfo, Error,
+	NegativeImbalanceOf, NominatorSlashInEra, OffenceQueue, OffenceQueueEras, PagedExposure,
+	Pallet, Perbill, ProcessingOffence, SessionInterface, SlashRewardFraction, SpanSlash,
+	UnappliedSlash, UnappliedSlashes, ValidatorSlashInEra,
 };
 use alloc::vec::Vec;
 use codec::{Decode, Encode, MaxEncodedLen};
 use frame_support::{
 	ensure,
-	traits::{Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
+	traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced},
 };
 use scale_info::TypeInfo;
 use sp_runtime::{
 	traits::{Saturating, Zero},
-	DispatchResult, RuntimeDebug,
+	DispatchResult, RuntimeDebug, WeakBoundedVec, Weight,
 };
 use sp_staking::{offence::OffenceSeverity, EraIndex, StakingInterface};
 
@@ -209,8 +210,12 @@ 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 Exposure<T::AccountId, BalanceOf<T>>,
+	pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
 	/// The era where the offence occurred.
 	pub(crate) slash_era: EraIndex,
 	/// The first era in the current bonding period.
@@ -222,78 +227,248 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> {
 	pub(crate) reward_proportion: Perbill,
 }
 
-/// 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.
+/// 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.
 ///
-/// 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();
+/// 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))
+		}
 
-	// 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
+		// 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))
 	}
 
-	let prior_slash_p = ValidatorSlashInEra::<T>::get(&params.slash_era, params.stash)
-		.map_or(Zero::zero(), |(prior_slash_proportion, _)| prior_slash_proportion);
+	// 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()
+				},
+			));
+		}
 
-	// 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),
-		);
-	} 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
+		// Remove from `OffenceQueue`
+		OffenceQueue::<T>::remove(oldest_era, &validator);
 	}
 
-	// apply slash to validator.
-	{
-		let mut spans = fetch_spans::<T>(
-			params.stash,
-			params.window_start,
-			&mut reward_payout,
-			&mut val_slashed,
-			params.reward_proportion,
+	// 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);
+		}
+	}
+
+	next_offence.map(|(v, o)| (oldest_era, v, o))
+}
+
+/// 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,
 		);
+		// No slash to apply. Discard.
+		return consumed_weight
+	};
 
-		let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
+	<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
+		offence_era,
+		slash_era,
+		offender: offender.clone(),
+		page: slash_page,
+	});
 
-		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);
-		}
+	log!(
+		debug,
+		"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
+		offence_record.slash_fraction,
+		offence_era,
+		offence_record.reported_era,
+	);
+
+	// 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,
+		);
+
+		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,
+		);
+
+		add_db_reads_writes(0, 1);
+		UnappliedSlashes::<T>::insert(
+			slash_era,
+			(offender, offence_record.slash_fraction, slash_page),
+			unapplied,
+		);
 	}
 
-	add_offending_validator::<T>(&params);
+	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());
 
 	let mut nominators_slashed = Vec::new();
-	reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed);
+	let (nom_slashed, nom_reward_payout) =
+		slash_nominators::<T>(params.clone(), &mut nominators_slashed);
+	reward_payout += nom_reward_payout;
 
-	Some(UnappliedSlash {
+	(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
 		validator: params.stash.clone(),
 		own: val_slashed,
-		others: nominators_slashed,
-		reporters: Vec::new(),
+		others: WeakBoundedVec::force_from(
+			nominators_slashed,
+			Some("slashed nominators not expected to be larger than the bounds"),
+		),
+		reporter: None,
 		payout: reward_payout,
 	})
 }
@@ -316,17 +491,18 @@ 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`].
-fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
+pub(crate) fn add_offending_validator<T: Config>(
+	stash: &T::AccountId,
+	slash: Perbill,
+	offence_era: EraIndex,
+) {
 	DisabledValidators::<T>::mutate(|disabled| {
-		let new_severity = OffenceSeverity(params.slash);
-		let decision =
-			T::DisablingStrategy::decision(params.stash, new_severity, params.slash_era, &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
@@ -346,7 +522,7 @@ fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
 						T::SessionInterface::disable_validator(offender_idx);
 						// Emit event that a validator got disabled
 						<Pallet<T>>::deposit_event(super::Event::<T>::ValidatorDisabled {
-							stash: params.stash.clone(),
+							stash: stash.clone(),
 						});
 					}
 				},
@@ -373,25 +549,70 @@ fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
 	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.
+fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceOf<T>) {
+	let own_slash = params.slash * params.exposure.exposure_metadata.own;
+	log!(
+		warn,
+		"🦹 slashing validator {:?} of stake: {:?} with {:?}% for {:?} in era {:?}",
+		params.stash,
+		params.exposure.exposure_metadata.own,
+		params.slash,
+		own_slash,
+		params.slash_era,
+	);
+
+	if own_slash == 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 (Zero::zero(), Zero::zero())
+	}
+
+	// apply slash to validator.
+	let mut reward_payout = Zero::zero();
+	let mut val_slashed = Zero::zero();
+
+	{
+		let mut spans = fetch_spans::<T>(
+			params.stash,
+			params.window_start,
+			&mut reward_payout,
+			&mut val_slashed,
+			params.reward_proportion,
+		);
+
+		let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
+
+		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);
+		}
+	}
+
+	(val_slashed, reward_payout)
+}
+
 /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
 ///
-/// Returns the amount of reward to pay out.
+/// Returns the total amount slashed and 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> {
-	let mut reward_payout = Zero::zero();
+) -> (BalanceOf<T>, BalanceOf<T>) {
+	let mut reward_payout = BalanceOf::<T>::zero();
+	let mut total_slashed = BalanceOf::<T>::zero();
 
-	nominators_slashed.reserve(params.exposure.others.len());
-	for nominator in &params.exposure.others {
+	nominators_slashed.reserve(params.exposure.exposure_page.others.len());
+	for nominator in &params.exposure.exposure_page.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 = prior_slash_p * nominator.value;
+			let own_slash_prior = params.prior_slash * nominator.value;
 			let own_slash_by_validator = params.slash * nominator.value;
 			let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior);
 
@@ -421,9 +642,10 @@ fn slash_nominators<T: Config>(
 			}
 		}
 		nominators_slashed.push((stash.clone(), nom_slashed));
+		total_slashed.saturating_accrue(nom_slashed);
 	}
 
-	reward_payout
+	(total_slashed, reward_payout)
 }
 
 // helper struct for managing a set of spans we are currently inspecting.
@@ -637,22 +859,25 @@ pub fn do_slash<T: Config>(
 }
 
 /// Apply a previously-unapplied slash.
-pub(crate) fn apply_slash<T: Config>(
-	unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>,
-	slash_era: EraIndex,
-) {
+pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
 	let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
 	let mut reward_payout = unapplied_slash.payout;
 
-	do_slash::<T>(
-		&unapplied_slash.validator,
-		unapplied_slash.own,
-		&mut reward_payout,
-		&mut slashed_imbalance,
-		slash_era,
-	);
+	if unapplied_slash.own > Zero::zero() {
+		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,
@@ -662,7 +887,11 @@ pub(crate) fn apply_slash<T: Config>(
 		);
 	}
 
-	pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
+	pay_reporters::<T>(
+		reward_payout,
+		slashed_imbalance,
+		&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
+	);
 }
 
 /// 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 8fe3c8f1775..3afa7d8a9bf 100644
--- a/substrate/frame/staking/src/tests.rs
+++ b/substrate/frame/staking/src/tests.rs
@@ -44,7 +44,7 @@ use sp_runtime::{
 };
 use sp_staking::{
 	offence::{OffenceDetails, OnOffenceHandler},
-	SessionIndex,
+	SessionIndex, StakingInterface,
 };
 use substrate_test_utils::assert_eq_uvec;
 
@@ -748,10 +748,7 @@ fn nominators_also_get_slashed_pro_rata() {
 			let exposed_nominator = initial_exposure.others.first().unwrap().value;
 
 			// 11 goes offline
-			on_offence_now(
-				&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
-				&[slash_percent],
-			);
+			on_offence_now(&[offence_from(11, None)], &[slash_percent], true);
 
 			// both stakes must have been decreased.
 			assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake);
@@ -2448,13 +2445,7 @@ fn reward_validator_slashing_validator_does_not_overflow() {
 		);
 
 		// Check slashing
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(100)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), stake - 1);
 		assert_eq!(asset::stakeable_balance::<Test>(&2), 1);
@@ -2547,13 +2538,7 @@ fn era_is_always_same_length() {
 #[test]
 fn offence_doesnt_force_new_era() {
 	ExtBuilder::default().build_and_execute(|| {
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(5)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(5)], true);
 
 		assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 	});
@@ -2565,13 +2550,7 @@ 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(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(5)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(5)], true);
 
 		assert_eq!(ForceEra::<Test>::get(), Forcing::ForceAlways);
 	});
@@ -2589,13 +2568,7 @@ fn offence_deselects_validator_even_when_slash_is_zero() {
 			assert!(Session::validators().contains(&11));
 			assert!(<Validators<Test>>::contains_key(11));
 
-			on_offence_now(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
-				&[Perbill::from_percent(0)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 			assert!(is_disabled(11));
@@ -2615,16 +2588,10 @@ 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(
-			&[OffenceDetails {
-				offender: (11, Exposure { total: 500, own: 500, others: vec![] }),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(50)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true);
 
 		// The stash account should be slashed for 250 (50% of 500).
-		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000 - 250);
+		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000 / 2);
 	});
 }
 
@@ -2639,13 +2606,7 @@ 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(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
-				&[Perbill::from_percent(0)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 			assert!(is_disabled(11));
@@ -2661,14 +2622,7 @@ 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(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
-				&[Perbill::from_percent(0)],
-				1,
-			);
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(0)], 1, true);
 
 			// the validator doesn't get disabled for an old offence
 			assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
@@ -2678,13 +2632,11 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() {
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
 			on_offence_in_era(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
+				&[offence_from(11, None)],
 				// 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
@@ -2696,9 +2648,9 @@ fn validator_is_not_disabled_for_an_offence_in_previous_era() {
 }
 
 #[test]
-fn reporters_receive_their_slice() {
-	// This test verifies that the reporters of the offence receive their slice from the slashed
-	// amount.
+fn only_first_reporter_receive_the_slice() {
+	// This test verifies that the first reporter 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;
@@ -2706,19 +2658,16 @@ fn reporters_receive_their_slice() {
 		assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance);
 
 		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![1, 2],
-			}],
+			&[OffenceDetails { offender: (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;
-		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);
+		assert_eq!(asset::total_balance::<Test>(&1), 10 + reward);
+		assert_eq!(asset::total_balance::<Test>(&2), 20 + 0);
 	});
 }
 
@@ -2732,26 +2681,14 @@ fn subsequent_reports_in_same_span_pay_out_less() {
 
 		assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance);
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![1],
-			}],
-			&[Perbill::from_percent(20)],
-		);
+		on_offence_now(&[offence_from(11, Some(1))], &[Perbill::from_percent(20)], true);
 
 		// 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(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![1],
-			}],
-			&[Perbill::from_percent(50)],
-		);
+		on_offence_now(&[offence_from(11, Some(1))], &[Perbill::from_percent(50)], true);
 
 		let prior_payout = reward;
 
@@ -2779,17 +2716,9 @@ fn invulnerables_are_not_slashed() {
 			.collect();
 
 		on_offence_now(
-			&[
-				OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				},
-				OffenceDetails {
-					offender: (21, Staking::eras_stakers(active_era(), &21)),
-					reporters: vec![],
-				},
-			],
+			&[offence_from(11, None), offence_from(21, None)],
 			&[Perbill::from_percent(50), Perbill::from_percent(20)],
+			true,
 		);
 
 		// The validator 11 hasn't been slashed, but 21 has been.
@@ -2813,13 +2742,7 @@ fn dont_slash_if_fraction_is_zero() {
 	ExtBuilder::default().build_and_execute(|| {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(0)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
 
 		// The validator hasn't been slashed. The new era is not forced.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
@@ -2834,36 +2757,18 @@ fn only_slash_for_max_in_era() {
 	ExtBuilder::default().build_and_execute(|| {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(50)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true);
 
 		// 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(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(25)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true);
 
 		// The validator has not been slashed additionally.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 500);
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(60)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(60)], true);
 
 		// The validator got slashed 10% more.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 400);
@@ -2879,25 +2784,13 @@ fn garbage_collection_after_slashing() {
 		.build_and_execute(|| {
 			assert_eq!(asset::stakeable_balance::<Test>(&11), 2000);
 
-			on_offence_now(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
-				&[Perbill::from_percent(10)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
 
 			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(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
-				&[Perbill::from_percent(100)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true);
 
 			// validator and nominator slash in era are garbage-collected by era change,
 			// so we don't test those here.
@@ -2935,13 +2828,7 @@ 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;
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(now, &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-		);
+		add_slash(&11);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 900);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000 - (nominated_value / 10));
@@ -2979,14 +2866,7 @@ 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(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-			2,
-		);
+		on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(10)], 2, true);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 900);
 
@@ -3005,14 +2885,7 @@ 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(
-			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), &21)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(30)],
-			3,
-		);
+		on_offence_in_era(&[offence_from(21, None)], &[Perbill::from_percent(30)], 3, true);
 
 		// 11 was not further slashed, but 21 and 101 were.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 900);
@@ -3026,14 +2899,7 @@ 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(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(20)],
-			2,
-		);
+		on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(20)], 2, true);
 
 		// 11 was further slashed, but 21 and 101 were not.
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 800);
@@ -3060,13 +2926,7 @@ fn slashes_are_summed_across_spans() {
 
 		let get_span = |account| SlashingSpans::<Test>::get(&account).unwrap();
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), &21)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-		);
+		on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true);
 
 		let expected_spans = vec![
 			slashing::SlashingSpan { index: 1, start: 4, length: None },
@@ -3083,13 +2943,7 @@ fn slashes_are_summed_across_spans() {
 
 		assert_eq!(Staking::slashable_balance_of(&21), 900);
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), &21)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-		);
+		on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true);
 
 		let expected_spans = vec![
 			slashing::SlashingSpan { index: 2, start: 5, length: None },
@@ -3115,13 +2969,10 @@ fn deferred_slashes_are_deferred() {
 
 		System::reset_events();
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-		);
+		// 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);
 
 		// nominations are not removed regardless of the deferring.
 		assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3134,27 +2985,37 @@ fn deferred_slashes_are_deferred() {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
-		mock::start_active_era(3);
+		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);
-
-		// 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);
-
+		// trigger slashing by advancing block.
+		advance_blocks(1);
 		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(),
 			&[
-				Event::SlashReported { validator: 11, slash_era: 1, .. },
+				// era 3 elections
 				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 },
 			]
 		));
 	})
@@ -3166,25 +3027,26 @@ 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(
-			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
+			&[offence_from(11, None)],
 			&[Perbill::from_percent(10)],
-			1, // should be deferred for two full eras, and applied at the beginning of era 4.
+			1, // should be deferred for two eras, and applied at the beginning of era 3.
+			true,
 		);
 
-		mock::start_active_era(4);
+		mock::start_active_era(3);
+		// Slashes not applied yet. Will apply in the next block after era starts.
+		advance_blocks(1);
 
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::SlashReported { validator: 11, slash_era: 1, .. },
+				Event::OffenceReported { validator: 11, offence_era: 1, .. },
+				Event::SlashComputed { offence_era: 1, slash_era: 3, offender: 11, page: 0 },
 				..,
 				Event::Slashed { staker: 11, amount: 100 },
 				Event::Slashed { staker: 101, amount: 12 }
@@ -3198,9 +3060,6 @@ 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)));
@@ -3209,21 +3068,23 @@ fn retroactive_deferred_slashes_one_before() {
 		mock::start_active_era(3);
 		System::reset_events();
 		on_offence_in_era(
-			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
+			&[offence_from(11, None)],
 			&[Perbill::from_percent(10)],
-			2, // should be deferred for two full eras, and applied at the beginning of era 5.
+			2, // should be deferred for two eras, and applied before the beginning of era 4.
+			true,
 		);
 
 		mock::start_active_era(4);
 
 		assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000);
-		// slash happens after the next line.
+		// slash happens at next blocks.
+		advance_blocks(1);
 
-		mock::start_active_era(5);
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::SlashReported { validator: 11, slash_era: 2, .. },
+				Event::OffenceReported { validator: 11, offence_era: 2, .. },
+				Event::SlashComputed { offence_era: 2, slash_era: 4, offender: 11, page: 0 },
 				..,
 				Event::Slashed { staker: 11, amount: 100 },
 				Event::Slashed { staker: 101, amount: 12 }
@@ -3249,13 +3110,7 @@ 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(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-		);
+		on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
 
 		// now we chill
 		assert_ok!(Staking::chill(RuntimeOrigin::signed(101)));
@@ -3324,23 +3179,44 @@ 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 4.
-		on_offence_now(
-			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(10)],
-		);
+		// 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);
 
 		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 4 as well.
+		// reported later, but deferred to start of era 3 as well.
 		System::reset_events();
-		on_offence_in_era(
-			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(15)],
-			1,
+		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
+					}
+				),
+			]
 		);
 
 		// fails if empty
@@ -3349,8 +3225,13 @@ fn remove_deferred() {
 			Error::<Test>::EmptyTargets
 		);
 
-		// cancel one of them.
-		assert_ok!(Staking::cancel_deferred_slash(RuntimeOrigin::root(), 4, vec![0]));
+		// 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);
 
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
@@ -3360,23 +3241,29 @@ fn remove_deferred() {
 		assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 		assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
 
-		// at the start of era 4, slashes from era 1 are processed,
-		// after being deferred for at least 2 full eras.
-		mock::start_active_era(4);
+		// at the next blocks, slashes from era 1 are processed, 1 page a block,
+		// after being deferred for 2 eras.
+		advance_blocks(1);
 
 		// the first slash for 10% was cancelled, but the 15% one not.
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::SlashReported { validator: 11, slash_era: 1, .. },
+				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::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 = Perbill::from_percent(15);
+		let slash_15 = slash_fraction_two;
 		let initial_slash = slash_10 * nominated_value;
 
 		let total_slash = slash_15 * nominated_value;
@@ -3390,67 +3277,48 @@ fn remove_deferred() {
 
 #[test]
 fn remove_multi_deferred() {
-	ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| {
-		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);
+	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);
 
-		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);
 
-		on_offence_now(
-			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), &21)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(10)],
-		);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
 
-		on_offence_now(
-			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(25)],
-		);
+			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(10)], true);
 
-		on_offence_now(
-			&[OffenceDetails { offender: (42, exposure.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(25)],
-		);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true);
 
-		on_offence_now(
-			&[OffenceDetails { offender: (69, exposure.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(25)],
-		);
+			on_offence_now(&[offence_from(41, None)], &[Perbill::from_percent(25)], true);
 
-		assert_eq!(UnappliedSlashes::<Test>::get(&4).len(), 5);
+			on_offence_now(&[offence_from(51, None)], &[Perbill::from_percent(25)], true);
 
-		// 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
-		);
+			// there are 5 slashes to be applied in era 3.
+			assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&3).count(), 5);
 
-		assert_ok!(Staking::cancel_deferred_slash(RuntimeOrigin::root(), 4, vec![0, 2, 4]));
+			// 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),
+				]
+			));
 
-		let slashes = UnappliedSlashes::<Test>::get(&4);
-		assert_eq!(slashes.len(), 2);
-		assert_eq!(slashes[0].validator, 21);
-		assert_eq!(slashes[1].validator, 42);
-	})
+			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));
+		})
 }
 
 #[test]
@@ -3479,10 +3347,7 @@ 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(
-				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(10)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
 
 			assert_eq!(
 				staking_events_since_last_call(),
@@ -3490,12 +3355,13 @@ 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::SlashReported {
+					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(10),
-						slash_era: 1
+						offence_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 },
 				]
@@ -3537,23 +3403,14 @@ 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(
-				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-				&[Perbill::zero()],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::zero()], true);
 
 			// 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(
-				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(25)],
-			);
+			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(25)], true);
 
 			// it DOES NOT affect the nominator.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3564,18 +3421,19 @@ 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::SlashReported {
+					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(0),
-						slash_era: 1
+						offence_era: 1
 					},
 					Event::ValidatorDisabled { stash: 11 },
-					Event::SlashReported {
+					Event::OffenceReported {
 						validator: 21,
 						fraction: Perbill::from_percent(25),
-						slash_era: 1
+						offence_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 }
 				]
@@ -3598,18 +3456,11 @@ 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(
-				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-				&[Perbill::zero()],
-				now,
-			);
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::zero()], now, true);
 
 			// nomination remains untouched.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3619,18 +3470,10 @@ fn slashing_independent_of_disabling_validator() {
 
 			// --- Slash without disabling ---
 			// offence that slashes 50% of the bond (setup for next slash)
-			on_offence_in_era(
-				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(50)],
-				now,
-			);
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(50)], now, true);
 
 			// offence that slashes 25% of the bond but does not disable
-			on_offence_in_era(
-				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(25)],
-				now,
-			);
+			on_offence_in_era(&[offence_from(21, None)], &[Perbill::from_percent(25)], now, true);
 
 			// nomination remains untouched.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3645,24 +3488,26 @@ 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::SlashReported {
+					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(0),
-						slash_era: 1
+						offence_era: 1
 					},
 					Event::ValidatorDisabled { stash: 11 },
-					Event::SlashReported {
+					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(50),
-						slash_era: 1
+						offence_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::SlashReported {
+					Event::OffenceReported {
 						validator: 21,
 						fraction: Perbill::from_percent(25),
-						slash_era: 1
+						offence_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 }
 				]
@@ -3688,25 +3533,14 @@ 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
-
-			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)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(50)], true);
 
 			// 11 should be disabled because the byzantine threshold is 1
 			assert!(is_disabled(11));
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
-			on_offence_now(
-				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
-				&[Perbill::zero()],
-			);
+			on_offence_now(&[offence_from(21, None)], &[Perbill::zero()], true);
 
 			// 21 should not be disabled because the number of disabled validators will be above the
 			// byzantine threshold
@@ -3714,10 +3548,7 @@ fn offence_threshold_doesnt_plan_new_era() {
 
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
-			on_offence_now(
-				&[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }],
-				&[Perbill::zero()],
-			);
+			on_offence_now(&[offence_from(31, None)], &[Perbill::zero()], true);
 
 			// same for 31
 			assert!(!is_disabled(31));
@@ -3739,13 +3570,7 @@ 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);
 
-			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)],
-			);
+			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(25)], true);
 
 			// nominations are not updated.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3759,10 +3584,7 @@ fn disabled_validators_are_kept_disabled_for_whole_era() {
 			assert!(is_disabled(21));
 
 			// validator 11 commits an offence
-			on_offence_now(
-				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(25)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(25)], true);
 
 			// nominations are not updated.
 			assert_eq!(Nominators::<Test>::get(101).unwrap().targets, vec![11, 21]);
@@ -3878,14 +3700,9 @@ 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(
-				&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(0)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(0)], true);
 
 			assert_eq!(asset::stakeable_balance::<Test>(&11), 1000);
 			assert_eq!(asset::stakeable_balance::<Test>(&101), 2000);
@@ -4878,6 +4695,7 @@ 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
@@ -4900,7 +4718,7 @@ fn offences_weight_calculated_correctly() {
 			>,
 		> = (1..10)
 			.map(|i| OffenceDetails {
-				offender: (i, Staking::eras_stakers(active_era(), &i)),
+				offender: (i, ()),
 				reporters: vec![],
 			})
 			.collect();
@@ -4914,10 +4732,7 @@ fn offences_weight_calculated_correctly() {
 		);
 
 		// On Offence with one offenders, Applied
-		let one_offender = [OffenceDetails {
-			offender: (11, Staking::eras_stakers(active_era(), &11)),
-			reporters: vec![1],
-		}];
+		let one_offender = [offence_from(11, Some(1))];
 
 		let n = 1; // Number of offenders
 		let rw = 3 + 3 * n; // rw reads and writes
@@ -6951,13 +6766,7 @@ mod staking_interface {
 	#[test]
 	fn do_withdraw_unbonded_with_wrong_slash_spans_works_as_expected() {
 		ExtBuilder::default().build_and_execute(|| {
-			on_offence_now(
-				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), &11)),
-					reporters: vec![],
-				}],
-				&[Perbill::from_percent(100)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(100)], true);
 
 			assert_eq!(Staking::bonded(&11), Some(11));
 
@@ -7241,13 +7050,7 @@ mod staking_unchecked {
 				let exposed_nominator = initial_exposure.others.first().unwrap().value;
 
 				// 11 goes offline
-				on_offence_now(
-					&[OffenceDetails {
-						offender: (11, initial_exposure.clone()),
-						reporters: vec![],
-					}],
-					&[slash_percent],
-				);
+				on_offence_now(&[offence_from(11, None)], &[slash_percent], true);
 
 				let slash_amount = slash_percent * exposed_stake;
 				let validator_share =
@@ -7313,13 +7116,7 @@ mod staking_unchecked {
 				let nominator_stake = Staking::ledger(101.into()).unwrap().total;
 
 				// 11 goes offline
-				on_offence_now(
-					&[OffenceDetails {
-						offender: (11, initial_exposure.clone()),
-						reporters: vec![],
-					}],
-					&[slash_percent],
-				);
+				on_offence_now(&[offence_from(11, None)], &[slash_percent], true);
 
 				// both stakes must have been decreased to 0.
 				assert_eq!(Staking::ledger(101.into()).unwrap().active, 0);
@@ -8511,19 +8308,9 @@ 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(
-				&[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)],
-			);
+			on_offence_now(&[offence_from(11, None)], &[Perbill::from_percent(10)], true);
+			on_offence_now(&[offence_from(21, None)], &[Perbill::from_percent(20)], true);
 
 			// it does NOT affect the nominator.
 			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
@@ -8533,10 +8320,7 @@ fn reenable_lower_offenders_mock() {
 			assert!(is_disabled(21));
 
 			// offence with a higher slash
-			on_offence_now(
-				&[OffenceDetails { offender: (31, exposure_31.clone()), reporters: vec![] }],
-				&[Perbill::from_percent(50)],
-			);
+			on_offence_now(&[offence_from(31, None)], &[Perbill::from_percent(50)], true);
 
 			// First offender is no longer disabled
 			assert!(!is_disabled(11));
@@ -8551,29 +8335,32 @@ 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::SlashReported {
+					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(10),
-						slash_era: 1
+						offence_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::SlashReported {
+					Event::OffenceReported {
 						validator: 21,
 						fraction: Perbill::from_percent(20),
-						slash_era: 1
+						offence_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::SlashReported {
+					Event::OffenceReported {
 						validator: 31,
 						fraction: Perbill::from_percent(50),
-						slash_era: 1
+						offence_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 },
 				]
 			);
@@ -8592,33 +8379,17 @@ 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(
-				&[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)],
+				&[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,
 			);
 
 			// 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));
 
@@ -8628,28 +8399,31 @@ 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::SlashReported {
+					Event::OffenceReported {
 						validator: 11,
 						fraction: Perbill::from_percent(50),
-						slash_era: 1
+						offence_era: 1
 					},
 					Event::ValidatorDisabled { stash: 11 },
-					Event::Slashed { staker: 11, amount: 500 },
-					Event::Slashed { staker: 101, amount: 62 },
-					Event::SlashReported {
+					Event::OffenceReported {
 						validator: 21,
 						fraction: Perbill::from_percent(50),
-						slash_era: 1
+						offence_era: 1
 					},
 					Event::ValidatorDisabled { stash: 21 },
-					Event::Slashed { staker: 21, amount: 500 },
-					Event::Slashed { staker: 101, amount: 187 },
-					Event::SlashReported {
+					Event::OffenceReported {
 						validator: 31,
 						fraction: Perbill::from_percent(10),
-						slash_era: 1
+						offence_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 },
+					Event::Slashed { staker: 101, amount: 187 },
+					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 },
+					Event::Slashed { staker: 11, amount: 500 },
+					Event::Slashed { staker: 101, amount: 62 },
 				]
 			);
 		});
@@ -9245,3 +9019,409 @@ mod hold_migration {
 		});
 	}
 }
+
+mod paged_slashing {
+	use super::*;
+	use crate::slashing::OffenceRecord;
+
+	#[test]
+	fn offence_processed_in_multi_block() {
+		// Ensure each page is processed only once.
+		ExtBuilder::default()
+			.has_stakers(false)
+			.slash_defer_duration(3)
+			.build_and_execute(|| {
+				let base_stake = 1000;
+
+				// Create a validator:
+				bond_validator(11, base_stake);
+				assert_eq!(Validators::<Test>::count(), 1);
+
+				// Track the total exposure of 11.
+				let mut exposure_counter = base_stake;
+
+				// Exposure page size is 64, hence it creates 4 pages of exposure.
+				let expected_page_count = 4;
+				for i in 0..200 {
+					let bond_amount = base_stake + i as Balance;
+					bond_nominator(1000 + i, bond_amount, vec![11]);
+					// with multi page reward payout, payout exposure is same as total exposure.
+					exposure_counter += bond_amount;
+				}
+
+				mock::start_active_era(1);
+
+				assert_eq!(
+					ErasStakersOverview::<Test>::get(1, 11).expect("exposure should exist"),
+					PagedExposureMetadata {
+						total: exposure_counter,
+						own: base_stake,
+						page_count: expected_page_count,
+						nominator_count: 200,
+					}
+				);
+
+				mock::start_active_era(2);
+				System::reset_events();
+
+				// report an offence for 11 in era 1.
+				on_offence_in_era(
+					&[offence_from(11, None)],
+					&[Perbill::from_percent(10)],
+					1,
+					false,
+				);
+
+				// ensure offence is queued.
+				assert_eq!(
+					staking_events_since_last_call().as_slice(),
+					vec![Event::OffenceReported {
+						validator: 11,
+						fraction: Perbill::from_percent(10),
+						offence_era: 1
+					}]
+				);
+
+				// ensure offence queue has items.
+				assert_eq!(
+					OffenceQueue::<Test>::get(1, 11).unwrap(),
+					slashing::OffenceRecord {
+						reporter: None,
+						reported_era: 2,
+						// first page to be marked for processing.
+						exposure_page: expected_page_count - 1,
+						slash_fraction: Perbill::from_percent(10),
+						prior_slash_fraction: Perbill::zero(),
+					}
+				);
+
+				// The offence era is noted in the queue.
+				assert_eq!(OffenceQueueEras::<Test>::get().unwrap(), vec![1]);
+
+				// ensure Processing offence is empty yet.
+				assert_eq!(ProcessingOffence::<Test>::get(), None);
+
+				// ensure no unapplied slashes for era 4 (offence_era + slash_defer_duration).
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 0);
+
+				// Checkpoint 1: advancing to next block will compute the first page of slash.
+				advance_blocks(1);
+
+				// ensure the last page of offence is processed.
+				// (offence is processed in reverse order of pages)
+				assert_eq!(
+					staking_events_since_last_call().as_slice(),
+					vec![Event::SlashComputed {
+						offence_era: 1,
+						slash_era: 4,
+						offender: 11,
+						page: expected_page_count - 1
+					},]
+				);
+
+				// offender is removed from offence queue
+				assert_eq!(OffenceQueue::<Test>::get(1, 11), None);
+
+				// offence era is removed from queue.
+				assert_eq!(OffenceQueueEras::<Test>::get(), None);
+
+				// this offence is not completely processed yet, so it should be in processing.
+				assert_eq!(
+					ProcessingOffence::<Test>::get(),
+					Some((
+						1,
+						11,
+						OffenceRecord {
+							reporter: None,
+							reported_era: 2,
+							// page 3 is processed, next page to be processed is 2.
+							exposure_page: 2,
+							slash_fraction: Perbill::from_percent(10),
+							prior_slash_fraction: Perbill::zero(),
+						}
+					))
+				);
+
+				// unapplied slashes for era 4.
+				let slashes = UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>();
+				// only one unapplied slash exists.
+				assert_eq!(slashes.len(), 1);
+				let (slash_key, unapplied_slash) = &slashes[0];
+				// this is a unique key to ensure unapplied slash is not overwritten for multiple
+				// offence by offender in the same era.
+				assert_eq!(*slash_key, (11, Perbill::from_percent(10), expected_page_count - 1));
+
+				// validator own stake is only included in the first page. Since this is page 3,
+				// only nominators are slashed.
+				assert_eq!(unapplied_slash.own, 0);
+				assert_eq!(unapplied_slash.validator, 11);
+				assert_eq!(unapplied_slash.others.len(), 200 % 64);
+
+				// Checkpoint 2: advancing to next block will compute the second page of slash.
+				advance_blocks(1);
+
+				// offence queue still empty
+				assert_eq!(OffenceQueue::<Test>::get(1, 11), None);
+				assert_eq!(OffenceQueueEras::<Test>::get(), None);
+
+				// processing offence points to next page.
+				assert_eq!(
+					ProcessingOffence::<Test>::get(),
+					Some((
+						1,
+						11,
+						OffenceRecord {
+							reporter: None,
+							reported_era: 2,
+							// page 2 is processed, next page to be processed is 1.
+							exposure_page: 1,
+							slash_fraction: Perbill::from_percent(10),
+							prior_slash_fraction: Perbill::zero(),
+						}
+					))
+				);
+
+				// there are two unapplied slashes for era 4.
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 2);
+
+				// ensure the last page of offence is processed.
+				// (offence is processed in reverse order of pages)
+				assert_eq!(
+					staking_events_since_last_call().as_slice(),
+					vec![Event::SlashComputed {
+						offence_era: 1,
+						slash_era: 4,
+						offender: 11,
+						page: expected_page_count - 2
+					},]
+				);
+
+				// Checkpoint 3: advancing to two more blocks will complete the processing of the
+				// reported offence
+				advance_blocks(2);
+
+				// no processing offence.
+				assert!(ProcessingOffence::<Test>::get().is_none());
+				// total of 4 unapplied slash.
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 4);
+
+				// Checkpoint 4: lets verify the application of slashes in multiple blocks.
+				// advance to era 4.
+				mock::start_active_era(4);
+				// slashes are not applied just yet. From next blocks, they will be applied.
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 4);
+
+				// advance to next block.
+				advance_blocks(1);
+				// 1 slash is applied.
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 3);
+
+				// advance two blocks.
+				advance_blocks(2);
+				// 2 more slashes are applied.
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 1);
+
+				// advance one more block.
+				advance_blocks(1);
+				// all slashes are applied.
+				assert_eq!(UnappliedSlashes::<Test>::iter_prefix(&4).collect::<Vec<_>>().len(), 0);
+
+				// ensure all stakers are slashed correctly.
+				assert_eq!(asset::staked::<Test>(&11), 1000 - 100);
+
+				for i in 0..200 {
+					let original_stake = 1000 + i as Balance;
+					let expected_slash = Perbill::from_percent(10) * original_stake;
+					assert_eq!(asset::staked::<Test>(&(1000 + i)), original_stake - expected_slash);
+				}
+			})
+	}
+
+	#[test]
+	fn offence_discarded_correctly() {
+		ExtBuilder::default().slash_defer_duration(3).build_and_execute(|| {
+			start_active_era(2);
+
+			// Scenario 1: 11 commits an offence in era 2.
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(10)], 2, false);
+
+			// offence is queued, not processed yet.
+			let queued_offence_one = OffenceQueue::<Test>::get(2, 11).unwrap();
+			assert_eq!(queued_offence_one.slash_fraction, Perbill::from_percent(10));
+			assert_eq!(queued_offence_one.prior_slash_fraction, Perbill::zero());
+			assert_eq!(OffenceQueueEras::<Test>::get().unwrap(), vec![2]);
+
+			// Scenario 1A: 11 commits a second offence in era 2 with **lower** slash fraction than
+			// the previous offence.
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(5)], 2, false);
+
+			// the second offence is discarded. No change in the queue.
+			assert_eq!(OffenceQueue::<Test>::get(2, 11).unwrap(), queued_offence_one);
+
+			// Scenario 1B: 11 commits a second offence in era 2 with **higher** slash fraction than
+			// the previous offence.
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(15)], 2, false);
+
+			// the second offence overwrites the first offence.
+			let overwritten_offence = OffenceQueue::<Test>::get(2, 11).unwrap();
+			assert!(overwritten_offence.slash_fraction > queued_offence_one.slash_fraction);
+			assert_eq!(overwritten_offence.slash_fraction, Perbill::from_percent(15));
+			assert_eq!(overwritten_offence.prior_slash_fraction, Perbill::zero());
+			assert_eq!(OffenceQueueEras::<Test>::get().unwrap(), vec![2]);
+
+			// Scenario 2: 11 commits another offence in era 2, but after the previous offence is
+			// processed.
+			advance_blocks(1);
+			assert!(OffenceQueue::<Test>::get(2, 11).is_none());
+			assert!(OffenceQueueEras::<Test>::get().is_none());
+			// unapplied slash is created for the offence.
+			assert!(UnappliedSlashes::<Test>::contains_key(
+				2 + 3,
+				(11, Perbill::from_percent(15), 0)
+			));
+
+			// Scenario 2A: offence has **lower** slash fraction than the previous offence.
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(14)], 2, false);
+			// offence is discarded.
+			assert!(OffenceQueue::<Test>::get(2, 11).is_none());
+			assert!(OffenceQueueEras::<Test>::get().is_none());
+
+			// Scenario 2B: offence has **higher** slash fraction than the previous offence.
+			on_offence_in_era(&[offence_from(11, None)], &[Perbill::from_percent(16)], 2, false);
+			// process offence
+			advance_blocks(1);
+			// there are now two slash records for 11, for era 5, with the newer one only slashing
+			// the diff between slash fractions of 16 and 15.
+			let slash_one =
+				UnappliedSlashes::<Test>::get(2 + 3, (11, Perbill::from_percent(15), 0)).unwrap();
+			let slash_two =
+				UnappliedSlashes::<Test>::get(2 + 3, (11, Perbill::from_percent(16), 0)).unwrap();
+			assert!(slash_one.own > slash_two.own);
+		});
+	}
+
+	#[test]
+	fn offence_eras_queued_correctly() {
+		ExtBuilder::default().build_and_execute(|| {
+			// 11 and 21 are validators.
+			assert_eq!(Staking::status(&11).unwrap(), StakerStatus::Validator);
+			assert_eq!(Staking::status(&21).unwrap(), StakerStatus::Validator);
+
+			start_active_era(2);
+
+			// 11 and 21 commits offence in era 2.
+			on_offence_in_era(
+				&[offence_from(11, None), offence_from(21, None)],
+				&[Perbill::from_percent(10), Perbill::from_percent(20)],
+				2,
+				false,
+			);
+
+			// 11 and 21 commits offence in era 1 but reported after the era 2 offence.
+			on_offence_in_era(
+				&[offence_from(11, None), offence_from(21, None)],
+				&[Perbill::from_percent(10), Perbill::from_percent(20)],
+				1,
+				false,
+			);
+
+			// queued offence eras are sorted.
+			assert_eq!(OffenceQueueEras::<Test>::get().unwrap(), vec![1, 2]);
+
+			// next two blocks, the offence in era 1 is processed.
+			advance_blocks(2);
+
+			// only era 2 is left in the queue.
+			assert_eq!(OffenceQueueEras::<Test>::get().unwrap(), vec![2]);
+
+			// next block, the offence in era 2 is processed.
+			advance_blocks(1);
+
+			// era still exist in the queue.
+			assert_eq!(OffenceQueueEras::<Test>::get().unwrap(), vec![2]);
+
+			// next block, the era 2 is processed.
+			advance_blocks(1);
+
+			// queue is empty.
+			assert_eq!(OffenceQueueEras::<Test>::get(), None);
+		});
+	}
+	#[test]
+	fn non_deferred_slash_applied_instantly() {
+		ExtBuilder::default().build_and_execute(|| {
+			mock::start_active_era(2);
+			let validator_stake = asset::staked::<Test>(&11);
+			let slash_fraction = Perbill::from_percent(10);
+			let expected_slash = slash_fraction * validator_stake;
+			System::reset_events();
+
+			// report an offence for 11 in era 1.
+			on_offence_in_era(&[offence_from(11, None)], &[slash_fraction], 1, false);
+
+			// ensure offence is queued.
+			assert_eq!(
+				staking_events_since_last_call().as_slice(),
+				vec![Event::OffenceReported {
+					validator: 11,
+					fraction: Perbill::from_percent(10),
+					offence_era: 1
+				}]
+			);
+
+			// process offence
+			advance_blocks(1);
+
+			// ensure slash is computed and applied.
+			assert_eq!(
+				staking_events_since_last_call().as_slice(),
+				vec![
+					Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 },
+					Event::Slashed { staker: 11, amount: expected_slash },
+					// this is the nominator of 11.
+					Event::Slashed { staker: 101, amount: 12 },
+				]
+			);
+
+			// ensure validator is slashed.
+			assert_eq!(asset::staked::<Test>(&11), validator_stake - expected_slash);
+		});
+	}
+
+	#[test]
+	fn validator_with_no_exposure_slashed() {
+		ExtBuilder::default().build_and_execute(|| {
+			let validator_stake = asset::staked::<Test>(&11);
+			let slash_fraction = Perbill::from_percent(10);
+			let expected_slash = slash_fraction * validator_stake;
+
+			// only 101 nominates 11, lets remove them.
+			assert_ok!(Staking::nominate(RuntimeOrigin::signed(101), vec![21]));
+
+			start_active_era(2);
+			// ensure validator has no exposure.
+			assert_eq!(ErasStakersOverview::<Test>::get(2, 11).unwrap().page_count, 0,);
+
+			// clear events
+			System::reset_events();
+
+			// report an offence for 11.
+			on_offence_now(&[offence_from(11, None)], &[slash_fraction], true);
+
+			// ensure validator is slashed.
+			assert_eq!(asset::staked::<Test>(&11), validator_stake - expected_slash);
+			assert_eq!(
+				staking_events_since_last_call().as_slice(),
+				vec![
+					Event::OffenceReported {
+						offence_era: 2,
+						validator: 11,
+						fraction: slash_fraction
+					},
+					Event::SlashComputed { offence_era: 2, slash_era: 2, offender: 11, page: 0 },
+					Event::Slashed { staker: 11, amount: expected_slash },
+				]
+			);
+		});
+	}
+}
diff --git a/substrate/frame/staking/src/weights.rs b/substrate/frame/staking/src/weights.rs
index 92fe0e176a2..36b7be74498 100644
--- a/substrate/frame/staking/src/weights.rs
+++ b/substrate/frame/staking/src/weights.rs
@@ -84,6 +84,7 @@ pub trait WeightInfo {
 	fn set_min_commission() -> Weight;
 	fn restore_ledger() -> Weight;
 	fn migrate_currency() -> Weight;
+	fn apply_slash() -> Weight;
 }
 
 /// Weights for `pallet_staking` using the Substrate node and recommended hardware.
@@ -815,6 +816,10 @@ 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))
 	}
+	fn apply_slash() -> Weight {
+		// TODO CI-FAIL: run CI bench bot
+		Weight::zero()
+	}
 }
 
 // For backwards compatibility and tests.
@@ -1545,4 +1550,8 @@ impl WeightInfo for () {
 			.saturating_add(RocksDbWeight::get().reads(6_u64))
 			.saturating_add(RocksDbWeight::get().writes(2_u64))
 	}
+	fn apply_slash() -> Weight {
+		// TODO CI-FAIL: run CI bench bot
+		Weight::zero()
+	}
 }
-- 
GitLab