diff --git a/polkadot/runtime/parachains/src/disputes/slashing.rs b/polkadot/runtime/parachains/src/disputes/slashing.rs
index d0c74e4bc958320fca78231129dd6e8e135e0179..a61d0c8998364c111f6e3a49fc03d5d39a5d76af 100644
--- a/polkadot/runtime/parachains/src/disputes/slashing.rs
+++ b/polkadot/runtime/parachains/src/disputes/slashing.rs
@@ -64,7 +64,7 @@ use sp_runtime::{
 	KeyTypeId, Perbill,
 };
 use sp_session::{GetSessionNumber, GetValidatorCount};
-use sp_staking::offence::{DisableStrategy, Kind, Offence, OffenceError, ReportOffence};
+use sp_staking::offence::{Kind, Offence, OffenceError, ReportOffence};
 use sp_std::{
 	collections::{btree_map::Entry, btree_set::BTreeSet},
 	prelude::*,
@@ -134,15 +134,6 @@ where
 		self.time_slot.clone()
 	}
 
-	fn disable_strategy(&self) -> DisableStrategy {
-		match self.kind {
-			SlashingOffenceKind::ForInvalid => DisableStrategy::Always,
-			// in the future we might change it based on number of disputes initiated:
-			// <https://github.com/paritytech/polkadot/issues/5946>
-			SlashingOffenceKind::AgainstValid => DisableStrategy::Never,
-		}
-	}
-
 	fn slash_fraction(&self, _offenders: u32) -> Perbill {
 		self.slash_fraction
 	}
diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs
index 514643c0a20169291cde30fd9dfc259bc936853f..d0f1ff0035fcf37cb57c8be5f7effb5d1742c847 100644
--- a/polkadot/runtime/test-runtime/src/lib.rs
+++ b/polkadot/runtime/test-runtime/src/lib.rs
@@ -313,7 +313,6 @@ parameter_types! {
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
 	pub const MaxExposurePageSize: u32 = 64;
 	pub const MaxNominators: u32 = 256;
-	pub storage OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
 	pub const MaxAuthorities: u32 = 100_000;
 	pub const OnChainMaxWinners: u32 = u32::MAX;
 	// Unbounded number of election targets and voters.
@@ -349,7 +348,6 @@ impl pallet_staking::Config for Runtime {
 	type SessionInterface = Self;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type MaxExposurePageSize = MaxExposurePageSize;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
@@ -364,6 +362,7 @@ impl pallet_staking::Config for Runtime {
 	type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig;
 	type EventListeners = ();
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 parameter_types! {
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index 7924939c79bdf97127685c7bcb0d9f2004267b9e..03ecd5c070b20fedf19fc55cca2403a3e87fc495 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -613,7 +613,6 @@ parameter_types! {
 	// this is an unbounded number. We just set it to a reasonably high value, 1 full page
 	// of nominators.
 	pub const MaxNominators: u32 = 64;
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
 	pub const MaxNominations: u32 = <NposCompactSolution16 as frame_election_provider_support::NposSolution>::LIMIT as u32;
 	pub const MaxControllersInDeprecationBatch: u32 = 751;
 }
@@ -634,7 +633,6 @@ impl pallet_staking::Config for Runtime {
 	type SessionInterface = Self;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type MaxExposurePageSize = MaxExposurePageSize;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = ElectionProviderMultiPhase;
 	type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
@@ -647,6 +645,7 @@ impl pallet_staking::Config for Runtime {
 	type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig;
 	type EventListeners = NominationPools;
 	type WeightInfo = weights::pallet_staking::WeightInfo<Runtime>;
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_fast_unstake::Config for Runtime {
@@ -1649,7 +1648,7 @@ pub mod migrations {
 	}
 
 	/// Unreleased migrations. Add new ones here:
-	pub type Unreleased = ();
+	pub type Unreleased = (pallet_staking::migrations::v15::MigrateV14ToV15<Runtime>,);
 }
 
 /// Unchecked extrinsic type as expected by this runtime.
diff --git a/polkadot/zombienet_tests/functional/0010-validator-disabling.toml b/polkadot/zombienet_tests/functional/0010-validator-disabling.toml
index c9d79c5f8f236918cf409fd684b7d0b8d9792d33..806f34d7f7670d9c654837f144f845352c7b41e7 100644
--- a/polkadot/zombienet_tests/functional/0010-validator-disabling.toml
+++ b/polkadot/zombienet_tests/functional/0010-validator-disabling.toml
@@ -21,7 +21,7 @@ requests = { memory = "2G", cpu = "1" }
   [[relaychain.node_groups]]
   name = "honest-validator"
   count = 3
-  args = ["-lparachain=debug"]
+  args = ["-lparachain=debug,runtime::staking=debug"]
 
   [[relaychain.node_groups]]
   image = "{{MALUS_IMAGE}}"
diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..f03540a50f6cc986ed529b80453a94ac4ee79acb
--- /dev/null
+++ b/prdoc/pr_2226.prdoc
@@ -0,0 +1,28 @@
+title: Validator disabling strategy in runtime
+
+doc:
+  - audience: Node Operator
+    description: |
+      On each committed offence (no matter slashable or not) the offending validator will be
+      disabled for a whole era.
+  - audience: Runtime Dev
+    description: |
+      The disabling strategy in staking pallet is no longer hardcoded but abstracted away via
+      `DisablingStrategy` trait. The trait contains a single function (make_disabling_decision) which
+      is called for each offence. The function makes a decision if (and which) validators should be
+      disabled. A default implementation is provided - `UpToLimitDisablingStrategy`. It
+      will be used on Kusama and Polkadot. In nutshell `UpToLimitDisablingStrategy`
+      disables offenders up to the configured threshold. Offending validators are not disabled for
+      offences in previous eras. The threshold is controlled via `DISABLING_LIMIT_FACTOR` (a generic
+      parameter of `UpToLimitDisablingStrategy`).
+
+migrations:
+  db: []
+  runtime:
+    - reference: pallet-staking
+      description: |
+        Renames `OffendingValidators` storage item to `DisabledValidators` and changes its type from
+        `Vec<(u32, bool)>` to `Vec<u32>`.
+
+crates:
+  - name: pallet-staking
\ 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 43c617023bcbd3351ac6ce40c9ad5d02a80ecd4a..0caaa8c7322626f40358067e72f0bd75b4fc23cf 100644
--- a/substrate/bin/node/runtime/src/lib.rs
+++ b/substrate/bin/node/runtime/src/lib.rs
@@ -654,7 +654,6 @@ parameter_types! {
 	pub const SlashDeferDuration: sp_staking::EraIndex = 24 * 7; // 1/4 the bonding duration.
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
 	pub const MaxNominators: u32 = 64;
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
 	pub const MaxControllersInDeprecationBatch: u32 = 5900;
 	pub OffchainRepeat: BlockNumber = 5;
 	pub HistoryDepth: u32 = 84;
@@ -690,7 +689,6 @@ impl pallet_staking::Config for Runtime {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
 	type MaxExposurePageSize = ConstU32<256>;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = ElectionProviderMultiPhase;
 	type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type VoterList = VoterList;
@@ -703,6 +701,7 @@ impl pallet_staking::Config for Runtime {
 	type EventListeners = NominationPools;
 	type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
 	type BenchmarkingConfig = StakingBenchmarkingConfig;
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_fast_unstake::Config for Runtime {
diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs
index ec54275278eb625d9648b9fd744a3f7aab0a646a..395a86e6528807ff21ee2667d1446016b6826a1e 100644
--- a/substrate/frame/babe/src/mock.rs
+++ b/substrate/frame/babe/src/mock.rs
@@ -144,7 +144,6 @@ parameter_types! {
 	pub const BondingDuration: EraIndex = 3;
 	pub const SlashDeferDuration: EraIndex = 0;
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16);
 	pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
 }
 
@@ -174,7 +173,6 @@ impl pallet_staking::Config for Test {
 	type UnixTime = pallet_timestamp::Pallet<Test>;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -187,6 +185,7 @@ impl pallet_staking::Config for Test {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_offences::Config for Test {
diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs
index 1c55adc8de4b7d87535367e1f934c9db171b4902..0b87de6bf5d79a3884d96f6f7cf79fc96ab3568b 100644
--- a/substrate/frame/beefy/src/mock.rs
+++ b/substrate/frame/beefy/src/mock.rs
@@ -158,7 +158,6 @@ parameter_types! {
 	pub const SessionsPerEra: SessionIndex = 3;
 	pub const BondingDuration: EraIndex = 3;
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
 	pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
 }
 
@@ -188,7 +187,6 @@ impl pallet_staking::Config for Test {
 	type UnixTime = pallet_timestamp::Pallet<Test>;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -201,6 +199,7 @@ impl pallet_staking::Config for Test {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_offences::Config for Test {
diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs
index 83083c912094bc4b39287f5105021fe2e44a4bb0..c00bb66ea13044f8b11aa781e30896e03225aea8 100644
--- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs
+++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs
@@ -23,7 +23,6 @@ pub(crate) const LOG_TARGET: &str = "tests::e2e-epm";
 use frame_support::{assert_err, assert_noop, assert_ok};
 use mock::*;
 use sp_core::Get;
-use sp_npos_elections::{to_supports, StakedAssignment};
 use sp_runtime::Perbill;
 
 use crate::mock::RuntimeOrigin;
@@ -127,75 +126,48 @@ fn offchainify_works() {
 }
 
 #[test]
-/// Replicates the Kusama incident of 8th Dec 2022 and its resolution through the governance
+/// Inspired by the Kusama incident of 8th Dec 2022 and its resolution through the governance
 /// fallback.
 ///
-/// After enough slashes exceeded the `Staking::OffendingValidatorsThreshold`, the staking pallet
-/// set `Forcing::ForceNew`. When a new session starts, staking will start to force a new era and
-/// calls <EPM as election_provider>::elect(). If at this point EPM and the staking miners did not
-/// have enough time to queue a new solution (snapshot + solution submission), the election request
-/// fails. If there is no election fallback mechanism in place, EPM enters in emergency mode.
-/// Recovery: Once EPM is in emergency mode, subsequent calls to `elect()` will fail until a new
-/// solution is added to EPM's `QueuedSolution` queue. This can be achieved through
-/// `Call::set_emergency_election_result` or `Call::governance_fallback` dispatchables. Once a new
-/// solution is added to the queue, EPM phase transitions to `Phase::Off` and the election flow
-/// restarts. Note that in this test case, the emergency throttling is disabled.
-fn enters_emergency_phase_after_forcing_before_elect() {
+/// Mass slash of validators shouldn't disable more than 1/3 of them (the byzantine threshold). Also
+/// no new era should be forced which could lead to EPM entering emergency mode.
+fn mass_slash_doesnt_enter_emergency_phase() {
 	let epm_builder = EpmExtBuilder::default().disable_emergency_throttling();
-	let (ext, pool_state, _) = ExtBuilder::default().epm(epm_builder).build_offchainify();
-
-	execute_with(ext, || {
-		log!(
-			trace,
-			"current validators (staking): {:?}",
-			<Runtime as pallet_staking::SessionInterface<AccountId>>::validators()
-		);
-		let session_validators_before = Session::validators();
-
-		roll_to_epm_off();
-		assert!(ElectionProviderMultiPhase::current_phase().is_off());
+	let staking_builder = StakingExtBuilder::default().validator_count(7);
+	let (mut ext, _, _) = ExtBuilder::default()
+		.epm(epm_builder)
+		.staking(staking_builder)
+		.build_offchainify();
 
+	ext.execute_with(|| {
 		assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
-		// slashes so that staking goes into `Forcing::ForceNew`.
-		slash_through_offending_threshold();
 
-		assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::ForceNew);
+		let active_set_size_before_slash = Session::validators().len();
 
-		advance_session_delayed_solution(pool_state.clone());
-		assert!(ElectionProviderMultiPhase::current_phase().is_emergency());
-		log_current_time();
+		// Slash more than 1/3 of the active validators
+		let mut slashed = slash_half_the_active_set();
 
-		let era_before_delayed_next = Staking::current_era();
-		// try to advance 2 eras.
-		assert!(start_next_active_era_delayed_solution(pool_state.clone()).is_ok());
-		assert_eq!(Staking::current_era(), era_before_delayed_next);
-		assert!(start_next_active_era(pool_state).is_err());
-		assert_eq!(Staking::current_era(), era_before_delayed_next);
+		let active_set_size_after_slash = Session::validators().len();
 
-		// EPM is still in emergency phase.
-		assert!(ElectionProviderMultiPhase::current_phase().is_emergency());
+		// active set should stay the same before and after the slash
+		assert_eq!(active_set_size_before_slash, active_set_size_after_slash);
 
-		// session validator set remains the same.
-		assert_eq!(Session::validators(), session_validators_before);
-
-		// performs recovery through the set emergency result.
-		let supports = to_supports(&vec![
-			StakedAssignment { who: 21, distribution: vec![(21, 10)] },
-			StakedAssignment { who: 31, distribution: vec![(21, 10), (31, 10)] },
-			StakedAssignment { who: 41, distribution: vec![(41, 10)] },
-		]);
-		assert!(ElectionProviderMultiPhase::set_emergency_election_result(
-			RuntimeOrigin::root(),
-			supports
-		)
-		.is_ok());
+		// Slashed validators are disabled up to a limit
+		slashed.truncate(
+			pallet_staking::UpToLimitDisablingStrategy::<SLASHING_DISABLING_FACTOR>::disable_limit(
+				active_set_size_after_slash,
+			),
+		);
 
-		// EPM can now roll to signed phase to proceed with elections. The validator set is the
-		// expected (ie. set through `set_emergency_election_result`).
-		roll_to_epm_signed();
-		//assert!(ElectionProviderMultiPhase::current_phase().is_signed());
-		assert_eq!(Session::validators(), vec![21, 31, 41]);
-		assert_eq!(Staking::current_era(), era_before_delayed_next.map(|e| e + 1));
+		// Find the indices of the disabled validators
+		let active_set = Session::validators();
+		let expected_disabled = slashed
+			.into_iter()
+			.map(|d| active_set.iter().position(|a| *a == d).unwrap() as u32)
+			.collect::<Vec<_>>();
+
+		assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
+		assert_eq!(Session::disabled_validators(), expected_disabled);
 	});
 }
 
@@ -253,77 +225,7 @@ fn continuous_slashes_below_offending_threshold() {
 }
 
 #[test]
-/// Slashed validator sets intentions in the same era of slashing.
-///
-/// When validators are slashed, they are chilled and removed from the current `VoterList`. Thus,
-/// the slashed validator should not be considered in the next validator set. However, if the
-/// slashed validator sets its intention to validate again in the same era when it was slashed and
-/// chilled, the validator may not be removed from the active validator set across eras, provided
-/// it would selected in the subsequent era if there was no slash. Nominators of the slashed
-/// validator will also be slashed and chilled, as expected, but the nomination intentions will
-/// remain after the validator re-set the intention to be validating again.
-///
-/// This behaviour is due to removing implicit chill upon slash
-/// <https://github.com/paritytech/substrate/pull/12420>.
-///
-/// Related to <https://github.com/paritytech/substrate/issues/13714>.
-fn set_validation_intention_after_chilled() {
-	use frame_election_provider_support::SortedListProvider;
-	use pallet_staking::{Event, Forcing, Nominators};
-
-	let (ext, pool_state, _) = ExtBuilder::default()
-		.epm(EpmExtBuilder::default())
-		.staking(StakingExtBuilder::default())
-		.build_offchainify();
-
-	execute_with(ext, || {
-		assert_eq!(active_era(), 0);
-		// validator is part of the validator set.
-		assert!(Session::validators().contains(&41));
-		assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
-
-		// nominate validator 81.
-		assert_ok!(Staking::nominate(RuntimeOrigin::signed(21), vec![41]));
-		assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
-
-		// validator is slashed. it is removed from the `VoterList` through chilling but in the
-		// current era, the validator is still part of the active validator set.
-		add_slash(&41);
-		assert!(Session::validators().contains(&41));
-		assert!(!<Runtime as pallet_staking::Config>::VoterList::contains(&41));
-		assert_eq!(
-			staking_events(),
-			[
-				Event::Chilled { stash: 41 },
-				Event::ForceEra { mode: Forcing::ForceNew },
-				Event::SlashReported {
-					validator: 41,
-					slash_era: 0,
-					fraction: Perbill::from_percent(10)
-				}
-			],
-		);
-
-		// after the nominator is slashed and chilled, the nominations remain.
-		assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
-
-		// validator sets intention to stake again in the same era it was chilled.
-		assert_ok!(Staking::validate(RuntimeOrigin::signed(41), Default::default()));
-
-		// progress era and check that the slashed validator is still part of the validator
-		// set.
-		assert!(start_next_active_era(pool_state).is_ok());
-		assert_eq!(active_era(), 1);
-		assert!(Session::validators().contains(&41));
-		assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
-
-		// nominations are still active as before the slash.
-		assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
-	})
-}
-
-#[test]
-/// Active ledger balance may fall below ED if account chills before unbonding.
+/// Active ledger balance may fall below ED if account chills before unbounding.
 ///
 /// Unbonding call fails if the remaining ledger's stash balance falls below the existential
 /// deposit. However, if the stash is chilled before unbonding, the ledger's active balance may
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 a727e3bf816251495c2b5e9e32eb688e51c4e339..8f1775a7e5951ad1c9271f486a782b18fe5da900 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
@@ -35,7 +35,7 @@ use sp_runtime::{
 	transaction_validity, BuildStorage, PerU16, Perbill, Percent,
 };
 use sp_staking::{
-	offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
+	offence::{OffenceDetails, OnOffenceHandler},
 	EraIndex, SessionIndex,
 };
 use sp_std::prelude::*;
@@ -236,7 +236,6 @@ parameter_types! {
 	pub const SessionsPerEra: sp_staking::SessionIndex = 2;
 	pub static BondingDuration: sp_staking::EraIndex = 28;
 	pub const SlashDeferDuration: sp_staking::EraIndex = 7; // 1/4 the bonding duration.
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(40);
 	pub HistoryDepth: u32 = 84;
 }
 
@@ -290,6 +289,8 @@ parameter_types! {
 
 /// Upper limit on the number of NPOS nominations.
 const MAX_QUOTA_NOMINATIONS: u32 = 16;
+/// Disabling factor set explicitly to byzantine threshold
+pub(crate) const SLASHING_DISABLING_FACTOR: usize = 3;
 
 impl pallet_staking::Config for Runtime {
 	type Currency = Balances;
@@ -308,7 +309,6 @@ impl pallet_staking::Config for Runtime {
 	type EraPayout = ();
 	type NextNewSession = Session;
 	type MaxExposurePageSize = ConstU32<256>;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = ElectionProviderMultiPhase;
 	type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type VoterList = BagsList;
@@ -320,6 +320,7 @@ impl pallet_staking::Config for Runtime {
 	type EventListeners = Pools;
 	type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy<SLASHING_DISABLING_FACTOR>;
 }
 
 impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
@@ -871,7 +872,6 @@ pub(crate) fn on_offence_now(
 		offenders,
 		slash_fraction,
 		Staking::eras_start_session_index(now).unwrap(),
-		DisableStrategy::WhenSlashed,
 	);
 }
 
@@ -886,19 +886,16 @@ pub(crate) fn add_slash(who: &AccountId) {
 	);
 }
 
-// Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`.
-pub(crate) fn slash_through_offending_threshold() {
-	let validators = Session::validators();
-	let mut remaining_slashes =
-		<Runtime as pallet_staking::Config>::OffendingValidatorsThreshold::get() *
-			validators.len() as u32;
+// Slashes 1/2 of the active set. Returns the `AccountId`s of the slashed validators.
+pub(crate) fn slash_half_the_active_set() -> Vec<AccountId> {
+	let mut slashed = Session::validators();
+	slashed.truncate(slashed.len() / 2);
 
-	for v in validators.into_iter() {
-		if remaining_slashes != 0 {
-			add_slash(&v);
-			remaining_slashes -= 1;
-		}
+	for v in slashed.iter() {
+		add_slash(v);
 	}
+
+	slashed
 }
 
 // Slashes a percentage of the active nominators that haven't been slashed yet, with
diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs
index b731cb822f336d6dd2da8e0d5ca501090909a31a..d876f9f6171e5ed55d9cbe95fd678fa071b5bd9b 100644
--- a/substrate/frame/fast-unstake/src/mock.rs
+++ b/substrate/frame/fast-unstake/src/mock.rs
@@ -134,7 +134,6 @@ impl pallet_staking::Config for Runtime {
 	type NextNewSession = ();
 	type HistoryDepth = ConstU32<84>;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = ();
 	type ElectionProvider = MockElection;
 	type GenesisElectionProvider = Self::ElectionProvider;
 	type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
@@ -145,6 +144,7 @@ impl pallet_staking::Config for Runtime {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 pub struct BalanceToU256;
diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs
index 4a21da655e5b3d4b219caed097619fa3268eca57..2d54f525b1f0c6b6a78f8df848c5770391f4fa2d 100644
--- a/substrate/frame/grandpa/src/mock.rs
+++ b/substrate/frame/grandpa/src/mock.rs
@@ -146,7 +146,6 @@ parameter_types! {
 	pub const SessionsPerEra: SessionIndex = 3;
 	pub const BondingDuration: EraIndex = 3;
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
 	pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
 }
 
@@ -176,7 +175,6 @@ impl pallet_staking::Config for Test {
 	type UnixTime = pallet_timestamp::Pallet<Test>;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -189,6 +187,7 @@ impl pallet_staking::Config for Test {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_offences::Config for Test {
diff --git a/substrate/frame/im-online/src/lib.rs b/substrate/frame/im-online/src/lib.rs
index 239b47834d1f8b1625a186e4a8f90e9861bfa303..f91a473e53d5389275974ed0e62c1d75d0daff54 100644
--- a/substrate/frame/im-online/src/lib.rs
+++ b/substrate/frame/im-online/src/lib.rs
@@ -104,7 +104,7 @@ use sp_runtime::{
 	PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion,
 };
 use sp_staking::{
-	offence::{DisableStrategy, Kind, Offence, ReportOffence},
+	offence::{Kind, Offence, ReportOffence},
 	SessionIndex,
 };
 use sp_std::prelude::*;
@@ -847,10 +847,6 @@ impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
 		self.session_index
 	}
 
-	fn disable_strategy(&self) -> DisableStrategy {
-		DisableStrategy::Never
-	}
-
 	fn slash_fraction(&self, offenders: u32) -> Perbill {
 		// the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07
 		// basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7%
diff --git a/substrate/frame/im-online/src/tests.rs b/substrate/frame/im-online/src/tests.rs
index f9959593494a0b94c4093f4c06f0e45c1e3e4f59..12333d59ef8959d26836330bb4a3a1be93ac49ab 100644
--- a/substrate/frame/im-online/src/tests.rs
+++ b/substrate/frame/im-online/src/tests.rs
@@ -50,9 +50,6 @@ fn test_unresponsiveness_slash_fraction() {
 		dummy_offence.slash_fraction(17),
 		Perbill::from_parts(46200000), // 4.62%
 	);
-
-	// Offline offences should never lead to being disabled.
-	assert_eq!(dummy_offence.disable_strategy(), DisableStrategy::Never);
 }
 
 #[test]
diff --git a/substrate/frame/nomination-pools/benchmarking/src/mock.rs b/substrate/frame/nomination-pools/benchmarking/src/mock.rs
index a59f8f3f40e7f6e67ca3163edb56b43257fdc6ee..2752d53a6b9f3364c234d9144a6c40b10b449a5b 100644
--- a/substrate/frame/nomination-pools/benchmarking/src/mock.rs
+++ b/substrate/frame/nomination-pools/benchmarking/src/mock.rs
@@ -111,7 +111,6 @@ impl pallet_staking::Config for Runtime {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = ();
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = ();
 	type ElectionProvider =
 		frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -124,6 +123,7 @@ impl pallet_staking::Config for Runtime {
 	type EventListeners = Pools;
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 parameter_types! {
diff --git a/substrate/frame/nomination-pools/test-staking/src/mock.rs b/substrate/frame/nomination-pools/test-staking/src/mock.rs
index 2ec47e0d164558defb0e70d260def626406680e7..93a05ddfae990108c7277c2448ff1470ae11d2ae 100644
--- a/substrate/frame/nomination-pools/test-staking/src/mock.rs
+++ b/substrate/frame/nomination-pools/test-staking/src/mock.rs
@@ -125,7 +125,6 @@ impl pallet_staking::Config for Runtime {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = ();
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = ();
 	type ElectionProvider =
 		frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -138,6 +137,7 @@ impl pallet_staking::Config for Runtime {
 	type EventListeners = Pools;
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 parameter_types! {
diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs
index 27129e73c71e85ee4af2ba1ed6414f30d0641f76..eeaa1364504abbb3aae3c2953d99f21952198b72 100644
--- a/substrate/frame/offences/benchmarking/src/mock.rs
+++ b/substrate/frame/offences/benchmarking/src/mock.rs
@@ -174,7 +174,6 @@ impl pallet_staking::Config for Test {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = ();
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
 	type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
@@ -186,6 +185,7 @@ impl pallet_staking::Config for Test {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_im_online::Config for Test {
diff --git a/substrate/frame/offences/src/lib.rs b/substrate/frame/offences/src/lib.rs
index 1c7ffeca7198325374f473cbae9e2f116697530c..a328b2fee4e2e72d3254f0a0a8283951bf5a5fc2 100644
--- a/substrate/frame/offences/src/lib.rs
+++ b/substrate/frame/offences/src/lib.rs
@@ -132,7 +132,6 @@ where
 			&concurrent_offenders,
 			&slash_perbill,
 			offence.session_index(),
-			offence.disable_strategy(),
 		);
 
 		// Deposit the event.
diff --git a/substrate/frame/offences/src/migration.rs b/substrate/frame/offences/src/migration.rs
index 3b5cf3ce926952df21248ea5b35271d6596a554b..199f47491369b8281e414076f35e43be823931c8 100644
--- a/substrate/frame/offences/src/migration.rs
+++ b/substrate/frame/offences/src/migration.rs
@@ -23,7 +23,7 @@ use frame_support::{
 	weights::Weight,
 	Twox64Concat,
 };
-use sp_staking::offence::{DisableStrategy, OnOffenceHandler};
+use sp_staking::offence::OnOffenceHandler;
 use sp_std::vec::Vec;
 
 #[cfg(feature = "try-runtime")]
@@ -106,12 +106,7 @@ pub fn remove_deferred_storage<T: Config>() -> Weight {
 	let deferred = <DeferredOffences<T>>::take();
 	log::info!(target: LOG_TARGET, "have {} deferred offences, applying.", deferred.len());
 	for (offences, perbill, session) in deferred.iter() {
-		let consumed = T::OnOffenceHandler::on_offence(
-			offences,
-			perbill,
-			*session,
-			DisableStrategy::WhenSlashed,
-		);
+		let consumed = T::OnOffenceHandler::on_offence(offences, perbill, *session);
 		weight = weight.saturating_add(consumed);
 	}
 
diff --git a/substrate/frame/offences/src/mock.rs b/substrate/frame/offences/src/mock.rs
index 31d5f805f3e4894407cd245db8bd7856d73c57cc..9a3120e41eaa0ae04ef4d211868c53782e3a4cbf 100644
--- a/substrate/frame/offences/src/mock.rs
+++ b/substrate/frame/offences/src/mock.rs
@@ -33,7 +33,7 @@ use sp_runtime::{
 	BuildStorage, Perbill,
 };
 use sp_staking::{
-	offence::{self, DisableStrategy, Kind, OffenceDetails},
+	offence::{self, Kind, OffenceDetails},
 	SessionIndex,
 };
 
@@ -51,7 +51,6 @@ impl<Reporter, Offender> offence::OnOffenceHandler<Reporter, Offender, Weight>
 		_offenders: &[OffenceDetails<Reporter, Offender>],
 		slash_fraction: &[Perbill],
 		_offence_session: SessionIndex,
-		_disable_strategy: DisableStrategy,
 	) -> Weight {
 		OnOffencePerbill::mutate(|f| {
 			*f = slash_fraction.to_vec();
diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs
index 24d259ed1d4ad57331e5f32d2f6bf6add6eb86d0..6531080b8d10436def07dfc3ae23e74c2b5962d4 100644
--- a/substrate/frame/root-offences/src/lib.rs
+++ b/substrate/frame/root-offences/src/lib.rs
@@ -33,7 +33,7 @@ use alloc::vec::Vec;
 use pallet_session::historical::IdentificationTuple;
 use pallet_staking::{BalanceOf, Exposure, ExposureOf, Pallet as Staking};
 use sp_runtime::Perbill;
-use sp_staking::offence::{DisableStrategy, OnOffenceHandler};
+use sp_staking::offence::OnOffenceHandler;
 
 pub use pallet::*;
 
@@ -128,7 +128,7 @@ pub mod pallet {
 				T::AccountId,
 				IdentificationTuple<T>,
 				Weight,
-			>>::on_offence(&offenders, &slash_fraction, session_index, DisableStrategy::WhenSlashed);
+			>>::on_offence(&offenders, &slash_fraction, session_index);
 		}
 	}
 }
diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs
index 626db138c2bf955b5d8b6b018f28f7e01acd753a..7e7332c3f7e3b39ca9457c6da05c7eb66197d0c4 100644
--- a/substrate/frame/root-offences/src/mock.rs
+++ b/substrate/frame/root-offences/src/mock.rs
@@ -133,7 +133,6 @@ parameter_types! {
 	pub static SlashDeferDuration: EraIndex = 0;
 	pub const BondingDuration: EraIndex = 3;
 	pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(75);
 }
 
 impl pallet_staking::Config for Test {
@@ -153,7 +152,6 @@ impl pallet_staking::Config for Test {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
 	type TargetList = pallet_staking::UseValidatorsMap<Self>;
@@ -165,6 +163,7 @@ impl pallet_staking::Config for Test {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl pallet_session::historical::Config for Test {
diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs
index 81052141fd8650106a2bbc68a5a67f2dbf457545..6cefa8f39a8c6081be0f5dfcec4b1d7ed0f8122c 100644
--- a/substrate/frame/session/benchmarking/src/mock.rs
+++ b/substrate/frame/session/benchmarking/src/mock.rs
@@ -174,7 +174,6 @@ impl pallet_staking::Config for Test {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
 	type MaxExposurePageSize = ConstU32<64>;
-	type OffendingValidatorsThreshold = ();
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
 	type MaxUnlockingChunks = ConstU32<32>;
@@ -186,6 +185,7 @@ impl pallet_staking::Config for Test {
 	type EventListeners = ();
 	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
 }
 
 impl crate::Config for Test {}
diff --git a/substrate/frame/session/src/lib.rs b/substrate/frame/session/src/lib.rs
index 17b6aa7a4640ce764919c7e20a07cb7ba1261486..9506e98adf7d70004a3caf57adb9c0c1dd44d5f3 100644
--- a/substrate/frame/session/src/lib.rs
+++ b/substrate/frame/session/src/lib.rs
@@ -627,7 +627,7 @@ impl<T: Config> Pallet<T> {
 		Validators::<T>::put(&validators);
 
 		if changed {
-			// reset disabled validators
+			// reset disabled validators if active set was changed
 			<DisabledValidators<T>>::take();
 		}
 
diff --git a/substrate/frame/staking/CHANGELOG.md b/substrate/frame/staking/CHANGELOG.md
index 719aa388755fce3523cadb273bb542e48076d3f5..113b7a6200b6e5b8014db37c0b43b7f45b96e62b 100644
--- a/substrate/frame/staking/CHANGELOG.md
+++ b/substrate/frame/staking/CHANGELOG.md
@@ -7,6 +7,25 @@ on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). We maintain a
 single integer version number for staking pallet to keep track of all storage
 migrations.
 
+## [v15]
+
+### Added
+
+- New trait `DisablingStrategy` which is responsible for making a decision which offenders should be
+  disabled on new offence.
+- Default implementation of `DisablingStrategy` - `UpToLimitDisablingStrategy`. It
+  disables each new offender up to a threshold (1/3 by default). Offenders are not runtime disabled for
+  offences in previous era(s). But they will be low-priority node-side disabled for dispute initiation.
+- `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all
+  offenders and if they are disabled or not. The latter just keeps a list of all offenders as they
+  are disabled by default.
+
+### Deprecated
+
+- `enum DisableStrategy` is no longer needed because disabling is not related to the type of the
+  offence anymore. A decision if a offender is disabled or not is made by a `DisablingStrategy`
+  implementation.
+
 ## [v14]
 
 ### Added
diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs
index f5b7e3eca3de7cd49b6a5cbdef37dfa4feff80ec..047ad6b87cc1322c9a67e3da316397d683ddd247 100644
--- a/substrate/frame/staking/src/lib.rs
+++ b/substrate/frame/staking/src/lib.rs
@@ -1239,3 +1239,79 @@ impl BenchmarkingConfig for TestBenchmarkingConfig {
 	type MaxValidators = frame_support::traits::ConstU32<100>;
 	type MaxNominators = frame_support::traits::ConstU32<100>;
 }
+
+/// Controls validator disabling
+pub trait DisablingStrategy<T: Config> {
+	/// Make a disabling decision. Returns the index of the validator to disable or `None` if no new
+	/// validator should be disabled.
+	fn decision(
+		offender_stash: &T::AccountId,
+		slash_era: EraIndex,
+		currently_disabled: &Vec<u32>,
+	) -> Option<u32>;
+}
+
+/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
+/// threshold. `DISABLING_LIMIT_FACTOR` is the factor of the maximum disabled validators in the
+/// active set. E.g. setting this value to `3` means no more than 1/3 of the validators in the
+/// active set can be disabled in an era.
+/// By default a factor of 3 is used which is the byzantine threshold.
+pub struct UpToLimitDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;
+
+impl<const DISABLING_LIMIT_FACTOR: usize> UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR> {
+	/// Disabling limit calculated from the total number of validators in the active set. When
+	/// reached no more validators will be disabled.
+	pub fn disable_limit(validators_len: usize) -> usize {
+		validators_len
+			.saturating_sub(1)
+			.checked_div(DISABLING_LIMIT_FACTOR)
+			.unwrap_or_else(|| {
+				defensive!("DISABLING_LIMIT_FACTOR should not be 0");
+				0
+			})
+	}
+}
+
+impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
+	for UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>
+{
+	fn decision(
+		offender_stash: &T::AccountId,
+		slash_era: EraIndex,
+		currently_disabled: &Vec<u32>,
+	) -> Option<u32> {
+		let active_set = T::SessionInterface::validators();
+
+		// We don't disable more than the limit
+		if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
+			log!(
+				debug,
+				"Won't disable: reached disabling limit {:?}",
+				Self::disable_limit(active_set.len())
+			);
+			return None
+		}
+
+		// We don't disable for offences in previous eras
+		if ActiveEra::<T>::get().map(|e| e.index).unwrap_or_default() > slash_era {
+			log!(
+				debug,
+				"Won't disable: current_era {:?} > slash_era {:?}",
+				Pallet::<T>::current_era().unwrap_or_default(),
+				slash_era
+			);
+			return None
+		}
+
+		let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) {
+			idx as u32
+		} else {
+			log!(debug, "Won't disable: offender not in active set",);
+			return None
+		};
+
+		log!(debug, "Will disable {:?}", offender_idx);
+
+		Some(offender_idx)
+	}
+}
diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs
index d5b18421d5b67fbeaac27cbbdecde174fa3d024b..510252be26c93e95e640af601ad8efdfd8801383 100644
--- a/substrate/frame/staking/src/migrations.rs
+++ b/substrate/frame/staking/src/migrations.rs
@@ -20,9 +20,10 @@
 use super::*;
 use frame_election_provider_support::SortedListProvider;
 use frame_support::{
+	migrations::VersionedMigration,
 	pallet_prelude::ValueQuery,
 	storage_alias,
-	traits::{GetStorageVersion, OnRuntimeUpgrade},
+	traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade},
 };
 
 #[cfg(feature = "try-runtime")]
@@ -59,11 +60,61 @@ impl Default for ObsoleteReleases {
 #[storage_alias]
 type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;
 
+/// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec<u32>`
+pub mod v15 {
+	use super::*;
+
+	// The disabling strategy used by staking pallet
+	type DefaultDisablingStrategy = UpToLimitDisablingStrategy;
+
+	pub struct VersionUncheckedMigrateV14ToV15<T>(sp_std::marker::PhantomData<T>);
+	impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15<T> {
+		fn on_runtime_upgrade() -> Weight {
+			let mut migrated = v14::OffendingValidators::<T>::take()
+				.into_iter()
+				.filter(|p| p.1) // take only disabled validators
+				.map(|p| p.0)
+				.collect::<Vec<_>>();
+
+			// Respect disabling limit
+			migrated.truncate(DefaultDisablingStrategy::disable_limit(
+				T::SessionInterface::validators().len(),
+			));
+
+			DisabledValidators::<T>::set(migrated);
+
+			log!(info, "v15 applied successfully.");
+			T::DbWeight::get().reads_writes(1, 1)
+		}
+
+		#[cfg(feature = "try-runtime")]
+		fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
+			frame_support::ensure!(
+				v14::OffendingValidators::<T>::decode_len().is_none(),
+				"OffendingValidators is not empty after the migration"
+			);
+			Ok(())
+		}
+	}
+
+	pub type MigrateV14ToV15<T> = VersionedMigration<
+		14,
+		15,
+		VersionUncheckedMigrateV14ToV15<T>,
+		Pallet<T>,
+		<T as frame_system::Config>::DbWeight,
+	>;
+}
+
 /// Migration of era exposure storage items to paged exposures.
 /// Changelog: [v14.](https://github.com/paritytech/substrate/blob/ankan/paged-rewards-rebased2/frame/staking/CHANGELOG.md#14)
 pub mod v14 {
 	use super::*;
 
+	#[frame_support::storage_alias]
+	pub(crate) type OffendingValidators<T: Config> =
+		StorageValue<Pallet<T>, Vec<(u32, bool)>, ValueQuery>;
+
 	pub struct MigrateToV14<T>(core::marker::PhantomData<T>);
 	impl<T: Config> OnRuntimeUpgrade for MigrateToV14<T> {
 		fn on_runtime_upgrade() -> Weight {
@@ -73,10 +124,10 @@ pub mod v14 {
 			if in_code == 14 && on_chain == 13 {
 				in_code.put::<Pallet<T>>();
 
-				log!(info, "v14 applied successfully.");
+				log!(info, "staking v14 applied successfully.");
 				T::DbWeight::get().reads_writes(1, 1)
 			} else {
-				log!(warn, "v14 not applied.");
+				log!(warn, "staking v14 not applied.");
 				T::DbWeight::get().reads(1)
 			}
 		}
diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs
index b46b863c016e5bf31213c903f46e3c05bfd1de5a..8c60dec65a81a123b5d1bd04b3ea8614ae1e9f0f 100644
--- a/substrate/frame/staking/src/mock.rs
+++ b/substrate/frame/staking/src/mock.rs
@@ -34,7 +34,7 @@ use frame_system::{EnsureRoot, EnsureSignedBy};
 use sp_io;
 use sp_runtime::{curve::PiecewiseLinear, testing::UintAuthorityId, traits::Zero, BuildStorage};
 use sp_staking::{
-	offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
+	offence::{OffenceDetails, OnOffenceHandler},
 	OnStakingUpdate,
 };
 
@@ -186,7 +186,6 @@ pallet_staking_reward_curve::build! {
 parameter_types! {
 	pub const BondingDuration: EraIndex = 3;
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
-	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(75);
 }
 
 parameter_types! {
@@ -267,6 +266,9 @@ impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
 	}
 }
 
+// Disabling threshold for `UpToLimitDisablingStrategy`
+pub(crate) const DISABLING_LIMIT_FACTOR: usize = 3;
+
 impl crate::pallet::pallet::Config for Test {
 	type Currency = Balances;
 	type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
@@ -284,7 +286,6 @@ impl crate::pallet::pallet::Config for Test {
 	type EraPayout = ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
 	type MaxExposurePageSize = MaxExposurePageSize;
-	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
 	// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
@@ -297,6 +298,7 @@ impl crate::pallet::pallet::Config for Test {
 	type EventListeners = EventListenerMock;
 	type BenchmarkingConfig = TestBenchmarkingConfig;
 	type WeightInfo = ();
+	type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>;
 }
 
 pub struct WeightedNominationsQuota<const MAX: u32>;
@@ -461,6 +463,8 @@ impl ExtBuilder {
 				(31, self.balance_factor * 2000),
 				(41, self.balance_factor * 2000),
 				(51, self.balance_factor * 2000),
+				(201, self.balance_factor * 2000),
+				(202, self.balance_factor * 2000),
 				// optional nominator
 				(100, self.balance_factor * 2000),
 				(101, self.balance_factor * 2000),
@@ -488,8 +492,10 @@ impl ExtBuilder {
 				(31, 31, self.balance_factor * 500, StakerStatus::<AccountId>::Validator),
 				// an idle validator
 				(41, 41, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
-			];
-			// optionally add a nominator
+				(51, 51, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
+				(201, 201, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
+				(202, 202, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
+			]; // optionally add a nominator
 			if self.nominate {
 				stakers.push((
 					101,
@@ -728,12 +734,11 @@ pub(crate) fn on_offence_in_era(
 	>],
 	slash_fraction: &[Perbill],
 	era: EraIndex,
-	disable_strategy: DisableStrategy,
 ) {
 	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, disable_strategy);
+			let _ = Staking::on_offence(offenders, slash_fraction, start_session);
 			return
 		} else if bonded_era > era {
 			break
@@ -745,7 +750,6 @@ pub(crate) fn on_offence_in_era(
 			offenders,
 			slash_fraction,
 			Staking::eras_start_session_index(era).unwrap(),
-			disable_strategy,
 		);
 	} else {
 		panic!("cannot slash in era {}", era);
@@ -760,7 +764,7 @@ pub(crate) fn on_offence_now(
 	slash_fraction: &[Perbill],
 ) {
 	let now = Staking::active_era().unwrap().index;
-	on_offence_in_era(offenders, slash_fraction, now, DisableStrategy::WhenSlashed)
+	on_offence_in_era(offenders, slash_fraction, now)
 }
 
 pub(crate) fn add_slash(who: &AccountId) {
diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs
index 0c0ef0dbf463c396e8f14c20cd1f87f276e5e39e..f4d4a7133dd5aacbf940ceec1f7feae2425061a8 100644
--- a/substrate/frame/staking/src/pallet/impls.rs
+++ b/substrate/frame/staking/src/pallet/impls.rs
@@ -43,7 +43,7 @@ use sp_runtime::{
 };
 use sp_staking::{
 	currency_to_vote::CurrencyToVote,
-	offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
+	offence::{OffenceDetails, OnOffenceHandler},
 	EraIndex, OnStakingUpdate, Page, SessionIndex, Stake,
 	StakingAccount::{self, Controller, Stash},
 	StakingInterface,
@@ -505,10 +505,8 @@ impl<T: Config> Pallet<T> {
 		}
 
 		// disable all offending validators that have been disabled for the whole era
-		for (index, disabled) in <OffendingValidators<T>>::get() {
-			if disabled {
-				T::SessionInterface::disable_validator(index);
-			}
+		for index in <DisabledValidators<T>>::get() {
+			T::SessionInterface::disable_validator(index);
 		}
 	}
 
@@ -598,8 +596,8 @@ impl<T: Config> Pallet<T> {
 			<ErasValidatorReward<T>>::insert(&active_era.index, validator_payout);
 			T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder));
 
-			// Clear offending validators.
-			<OffendingValidators<T>>::kill();
+			// Clear disabled validators.
+			<DisabledValidators<T>>::kill();
 		}
 	}
 
@@ -868,14 +866,6 @@ impl<T: Config> Pallet<T> {
 		Self::deposit_event(Event::<T>::ForceEra { mode });
 	}
 
-	/// Ensures that at the end of the current session there will be a new era.
-	pub(crate) fn ensure_new_era() {
-		match ForceEra::<T>::get() {
-			Forcing::ForceAlways | Forcing::ForceNew => (),
-			_ => Self::set_force_era(Forcing::ForceNew),
-		}
-	}
-
 	#[cfg(feature = "runtime-benchmarks")]
 	pub fn add_era_stakers(
 		current_era: EraIndex,
@@ -1447,7 +1437,6 @@ where
 		>],
 		slash_fraction: &[Perbill],
 		slash_session: SessionIndex,
-		disable_strategy: DisableStrategy,
 	) -> Weight {
 		let reward_proportion = SlashRewardFraction::<T>::get();
 		let mut consumed_weight = Weight::from_parts(0, 0);
@@ -1512,7 +1501,6 @@ where
 				window_start,
 				now: active_era,
 				reward_proportion,
-				disable_strategy,
 			});
 
 			Self::deposit_event(Event::<T>::SlashReported {
@@ -1986,7 +1974,8 @@ impl<T: Config> Pallet<T> {
 		Self::check_nominators()?;
 		Self::check_exposures()?;
 		Self::check_paged_exposures()?;
-		Self::check_count()
+		Self::check_count()?;
+		Self::ensure_disabled_validators_sorted()
 	}
 
 	/// Invariants:
@@ -2300,4 +2289,12 @@ impl<T: Config> Pallet<T> {
 
 		Ok(())
 	}
+
+	fn ensure_disabled_validators_sorted() -> Result<(), TryRuntimeError> {
+		ensure!(
+			DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] <= pair[1]),
+			"DisabledValidators is not sorted"
+		);
+		Ok(())
+	}
 }
diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs
index 76ddad6f1359a57e2fc361a010816724799f238b..9c968d88344413a250dead39fe9500029a3b5372 100644
--- a/substrate/frame/staking/src/pallet/mod.rs
+++ b/substrate/frame/staking/src/pallet/mod.rs
@@ -47,10 +47,11 @@ mod impls;
 pub use impls::*;
 
 use crate::{
-	slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout,
-	EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState, MaxNominationsOf,
-	NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination,
-	SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs,
+	slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, DisablingStrategy,
+	EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState,
+	MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf,
+	RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk,
+	ValidatorPrefs,
 };
 
 // The speculative number of spans are used as an input of the weight annotation of
@@ -67,7 +68,7 @@ pub mod pallet {
 	use super::*;
 
 	/// The in-code storage version.
-	const STORAGE_VERSION: StorageVersion = StorageVersion::new(14);
+	const STORAGE_VERSION: StorageVersion = StorageVersion::new(15);
 
 	#[pallet::pallet]
 	#[pallet::storage_version(STORAGE_VERSION)]
@@ -217,10 +218,6 @@ pub mod pallet {
 		#[pallet::constant]
 		type MaxExposurePageSize: Get<u32>;
 
-		/// The fraction of the validator set that is safe to be offending.
-		/// After the threshold is reached a new era will be forced.
-		type OffendingValidatorsThreshold: Get<Perbill>;
-
 		/// Something that provides a best-effort sorted list of voters aka electing nominators,
 		/// used for NPoS election.
 		///
@@ -278,6 +275,9 @@ pub mod pallet {
 		/// WARNING: this only reports slashing and withdraw events for the time being.
 		type EventListeners: sp_staking::OnStakingUpdate<Self::AccountId, BalanceOf<Self>>;
 
+		// `DisablingStragegy` controls how validators are disabled
+		type DisablingStrategy: DisablingStrategy<Self>;
+
 		/// Some parameters of the benchmarking.
 		type BenchmarkingConfig: BenchmarkingConfig;
 
@@ -654,19 +654,16 @@ pub mod pallet {
 	#[pallet::getter(fn current_planned_session)]
 	pub type CurrentPlannedSession<T> = StorageValue<_, SessionIndex, ValueQuery>;
 
-	/// Indices of validators that have offended in the active era and whether they are currently
-	/// disabled.
+	/// Indices of validators that have offended in the active era. The offenders are disabled for a
+	/// whole era. For this reason they are kept here - only staking pallet knows about eras. The
+	/// implementor of [`DisablingStrategy`] defines if a validator should be disabled which
+	/// implicitly means that the implementor also controls the max number of disabled validators.
 	///
-	/// This value should be a superset of disabled validators since not all offences lead to the
-	/// validator being disabled (if there was no slash). This is needed to track the percentage of
-	/// validators that have offended in the current era, ensuring a new era is forced if
-	/// `OffendingValidatorsThreshold` is reached. The vec is always kept sorted so that we can find
-	/// whether a given validator has previously offended using binary search. It gets cleared when
-	/// the era ends.
+	/// The vec is always kept sorted so that we can find whether a given validator has previously
+	/// offended using binary search.
 	#[pallet::storage]
 	#[pallet::unbounded]
-	#[pallet::getter(fn offending_validators)]
-	pub type OffendingValidators<T: Config> = StorageValue<_, Vec<(u32, bool)>, ValueQuery>;
+	pub type DisabledValidators<T: Config> = StorageValue<_, Vec<u32>, ValueQuery>;
 
 	/// The threshold for when users can start calling `chill_other` for other validators /
 	/// nominators. The threshold is compared to the actual number of validators / nominators
diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs
index 2011e9eb830144960488e7fe40dc82ed1fcfa1b6..f831f625957d4c495fce3b239e68faed737cb3ab 100644
--- a/substrate/frame/staking/src/slashing.rs
+++ b/substrate/frame/staking/src/slashing.rs
@@ -50,21 +50,21 @@
 //! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html>
 
 use crate::{
-	BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra,
-	OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash,
+	BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure, NegativeImbalanceOf,
+	NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash,
 	ValidatorSlashInEra,
 };
 use codec::{Decode, Encode, MaxEncodedLen};
 use frame_support::{
 	ensure,
-	traits::{Currency, Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced},
+	traits::{Currency, Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
 };
 use scale_info::TypeInfo;
 use sp_runtime::{
 	traits::{Saturating, Zero},
 	DispatchResult, RuntimeDebug,
 };
-use sp_staking::{offence::DisableStrategy, EraIndex};
+use sp_staking::EraIndex;
 use sp_std::vec::Vec;
 
 /// The proportion of the slashing reward to be paid out on the first slashing detection.
@@ -220,8 +220,6 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> {
 	/// The maximum percentage of a slash that ever gets paid out.
 	/// This is f_inf in the paper.
 	pub(crate) reward_proportion: Perbill,
-	/// When to disable offenders.
-	pub(crate) disable_strategy: DisableStrategy,
 }
 
 /// Computes a slash of a validator and nominators. It returns an unapplied
@@ -280,18 +278,13 @@ pub(crate) fn compute_slash<T: Config>(
 		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 - take appropriate
-			// actions.
-
-			// chill the validator - it misbehaved in the current span and should
-			// not continue in the next election. also end the slashing span.
+			// 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);
-			<Pallet<T>>::chill_stash(params.stash);
 		}
 	}
 
-	let disable_when_slashed = params.disable_strategy != DisableStrategy::Never;
-	add_offending_validator::<T>(params.stash, disable_when_slashed);
+	add_offending_validator::<T>(&params);
 
 	let mut nominators_slashed = Vec::new();
 	reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed);
@@ -320,54 +313,31 @@ fn kick_out_if_recent<T: Config>(params: SlashParams<T>) {
 	);
 
 	if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) {
+		// Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details
 		spans.end_span(params.now);
-		<Pallet<T>>::chill_stash(params.stash);
 	}
 
-	let disable_without_slash = params.disable_strategy == DisableStrategy::Always;
-	add_offending_validator::<T>(params.stash, disable_without_slash);
+	add_offending_validator::<T>(&params);
 }
 
-/// Add the given validator to the offenders list and optionally disable it.
-/// If after adding the validator `OffendingValidatorsThreshold` is reached
-/// a new era will be forced.
-fn add_offending_validator<T: Config>(stash: &T::AccountId, disable: bool) {
-	OffendingValidators::<T>::mutate(|offending| {
-		let validators = T::SessionInterface::validators();
-		let validator_index = match validators.iter().position(|i| i == stash) {
-			Some(index) => index,
-			None => return,
-		};
-
-		let validator_index_u32 = validator_index as u32;
-
-		match offending.binary_search_by_key(&validator_index_u32, |(index, _)| *index) {
-			// this is a new offending validator
-			Err(index) => {
-				offending.insert(index, (validator_index_u32, disable));
-
-				let offending_threshold =
-					T::OffendingValidatorsThreshold::get() * validators.len() as u32;
-
-				if offending.len() >= offending_threshold as usize {
-					// force a new era, to select a new validator set
-					<Pallet<T>>::ensure_new_era()
-				}
-
-				if disable {
-					T::SessionInterface::disable_validator(validator_index_u32);
-				}
-			},
-			Ok(index) => {
-				if disable && !offending[index].1 {
-					// the validator had previously offended without being disabled,
-					// let's make sure we disable it now
-					offending[index].1 = true;
-					T::SessionInterface::disable_validator(validator_index_u32);
-				}
-			},
+/// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of
+/// validators provided by [`make_disabling_decision`].
+fn add_offending_validator<T: Config>(params: &SlashParams<T>) {
+	DisabledValidators::<T>::mutate(|disabled| {
+		if let Some(offender) =
+			T::DisablingStrategy::decision(params.stash, params.slash_era, &disabled)
+		{
+			// Add the validator to `DisabledValidators` and disable it. Do nothing if it is
+			// already disabled.
+			if let Err(index) = disabled.binary_search_by_key(&offender, |index| *index) {
+				disabled.insert(index, offender);
+				T::SessionInterface::disable_validator(offender);
+			}
 		}
 	});
+
+	// `DisabledValidators` should be kept sorted
+	debug_assert!(DisabledValidators::<T>::get().windows(2).all(|pair| pair[0] < pair[1]));
 }
 
 /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs
index 87f6fd424bd75b6879bee1ee996a58b283da7ee5..6cf5a56e5a6df285f2d269b169ed15d6b573e622 100644
--- a/substrate/frame/staking/src/tests.rs
+++ b/substrate/frame/staking/src/tests.rs
@@ -38,7 +38,7 @@ use sp_runtime::{
 	Perbill, Percent, Perquintill, Rounding, TokenError,
 };
 use sp_staking::{
-	offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
+	offence::{OffenceDetails, OnOffenceHandler},
 	SessionIndex,
 };
 use sp_std::prelude::*;
@@ -716,56 +716,65 @@ fn nominating_and_rewards_should_work() {
 
 #[test]
 fn nominators_also_get_slashed_pro_rata() {
-	ExtBuilder::default().build_and_execute(|| {
-		mock::start_active_era(1);
-		let slash_percent = Perbill::from_percent(5);
-		let initial_exposure = Staking::eras_stakers(active_era(), &11);
-		// 101 is a nominator for 11
-		assert_eq!(initial_exposure.others.first().unwrap().who, 101);
-
-		// staked values;
-		let nominator_stake = Staking::ledger(101.into()).unwrap().active;
-		let nominator_balance = balances(&101).0;
-		let validator_stake = Staking::ledger(11.into()).unwrap().active;
-		let validator_balance = balances(&11).0;
-		let exposed_stake = initial_exposure.total;
-		let exposed_validator = initial_exposure.own;
-		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],
-		);
+	ExtBuilder::default()
+		.validator_count(4)
+		.set_status(41, StakerStatus::Validator)
+		.build_and_execute(|| {
+			mock::start_active_era(1);
+			let slash_percent = Perbill::from_percent(5);
+			let initial_exposure = Staking::eras_stakers(active_era(), &11);
+			// 101 is a nominator for 11
+			assert_eq!(initial_exposure.others.first().unwrap().who, 101);
+
+			// staked values;
+			let nominator_stake = Staking::ledger(101.into()).unwrap().active;
+			let nominator_balance = balances(&101).0;
+			let validator_stake = Staking::ledger(11.into()).unwrap().active;
+			let validator_balance = balances(&11).0;
+			let exposed_stake = initial_exposure.total;
+			let exposed_validator = initial_exposure.own;
+			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],
+			);
 
-		// both stakes must have been decreased.
-		assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake);
-		assert!(Staking::ledger(11.into()).unwrap().active < validator_stake);
+			// both stakes must have been decreased.
+			assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake);
+			assert!(Staking::ledger(11.into()).unwrap().active < validator_stake);
 
-		let slash_amount = slash_percent * exposed_stake;
-		let validator_share =
-			Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
-		let nominator_share =
-			Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
+			let slash_amount = slash_percent * exposed_stake;
+			let validator_share =
+				Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
+			let nominator_share =
+				Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
 
-		// both slash amounts need to be positive for the test to make sense.
-		assert!(validator_share > 0);
-		assert!(nominator_share > 0);
+			// both slash amounts need to be positive for the test to make sense.
+			assert!(validator_share > 0);
+			assert!(nominator_share > 0);
 
-		// both stakes must have been decreased pro-rata.
-		assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share);
-		assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share);
-		assert_eq!(
-			balances(&101).0, // free balance
-			nominator_balance - nominator_share,
-		);
-		assert_eq!(
-			balances(&11).0, // free balance
-			validator_balance - validator_share,
-		);
-		// Because slashing happened.
-		assert!(is_disabled(11));
-	});
+			// both stakes must have been decreased pro-rata.
+			assert_eq!(
+				Staking::ledger(101.into()).unwrap().active,
+				nominator_stake - nominator_share
+			);
+			assert_eq!(
+				Staking::ledger(11.into()).unwrap().active,
+				validator_stake - validator_share
+			);
+			assert_eq!(
+				balances(&101).0, // free balance
+				nominator_balance - nominator_share,
+			);
+			assert_eq!(
+				balances(&11).0, // free balance
+				validator_balance - validator_share,
+			);
+			// Because slashing happened.
+			assert!(is_disabled(11));
+		});
 }
 
 #[test]
@@ -2401,7 +2410,7 @@ fn era_is_always_same_length() {
 }
 
 #[test]
-fn offence_forces_new_era() {
+fn offence_doesnt_force_new_era() {
 	ExtBuilder::default().build_and_execute(|| {
 		on_offence_now(
 			&[OffenceDetails {
@@ -2411,7 +2420,7 @@ fn offence_forces_new_era() {
 			&[Perbill::from_percent(5)],
 		);
 
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
+		assert_eq!(Staking::force_era(), Forcing::NotForcing);
 	});
 }
 
@@ -2435,26 +2444,32 @@ fn offence_ensures_new_era_without_clobbering() {
 
 #[test]
 fn offence_deselects_validator_even_when_slash_is_zero() {
-	ExtBuilder::default().build_and_execute(|| {
-		assert!(Session::validators().contains(&11));
-		assert!(<Validators<Test>>::contains_key(11));
+	ExtBuilder::default()
+		.validator_count(7)
+		.set_status(41, StakerStatus::Validator)
+		.set_status(51, StakerStatus::Validator)
+		.set_status(201, StakerStatus::Validator)
+		.set_status(202, StakerStatus::Validator)
+		.build_and_execute(|| {
+			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(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				&[Perbill::from_percent(0)],
+			);
 
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
-		assert!(!<Validators<Test>>::contains_key(11));
+			assert_eq!(Staking::force_era(), Forcing::NotForcing);
+			assert!(is_disabled(11));
 
-		mock::start_active_era(1);
+			mock::start_active_era(1);
 
-		assert!(!Session::validators().contains(&11));
-		assert!(!<Validators<Test>>::contains_key(11));
-	});
+			// The validator should be reenabled in the new era
+			assert!(!is_disabled(11));
+		});
 }
 
 #[test]
@@ -2479,71 +2494,70 @@ fn slashing_performed_according_exposure() {
 }
 
 #[test]
-fn slash_in_old_span_does_not_deselect() {
-	ExtBuilder::default().build_and_execute(|| {
-		mock::start_active_era(1);
-
-		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)],
-		);
+fn validator_is_not_disabled_for_an_offence_in_previous_era() {
+	ExtBuilder::default()
+		.validator_count(4)
+		.set_status(41, StakerStatus::Validator)
+		.build_and_execute(|| {
+			mock::start_active_era(1);
 
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
-		assert!(!<Validators<Test>>::contains_key(11));
+			assert!(<Validators<Test>>::contains_key(11));
+			assert!(Session::validators().contains(&11));
 
-		mock::start_active_era(2);
+			on_offence_now(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				&[Perbill::from_percent(0)],
+			);
 
-		Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap();
-		assert_eq!(Staking::force_era(), Forcing::NotForcing);
-		assert!(<Validators<Test>>::contains_key(11));
-		assert!(!Session::validators().contains(&11));
+			assert_eq!(Staking::force_era(), Forcing::NotForcing);
+			assert!(is_disabled(11));
 
-		mock::start_active_era(3);
+			mock::start_active_era(2);
 
-		// this staker is in a new slashing span now, having re-registered after
-		// their prior slash.
+			// the validator is not disabled in the new era
+			Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap();
+			assert_eq!(Staking::force_era(), Forcing::NotForcing);
+			assert!(<Validators<Test>>::contains_key(11));
+			assert!(Session::validators().contains(&11));
 
-		on_offence_in_era(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			&[Perbill::from_percent(0)],
-			1,
-			DisableStrategy::WhenSlashed,
-		);
+			mock::start_active_era(3);
 
-		// the validator doesn't get chilled again
-		assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
+			// 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,
+			);
 
-		// but we are still forcing a new era
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
+			// the validator doesn't get disabled for an old offence
+			assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
+			assert!(!is_disabled(11));
 
-		on_offence_in_era(
-			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), &11)),
-				reporters: vec![],
-			}],
-			// NOTE: A 100% slash here would clean up the account, causing de-registration.
-			&[Perbill::from_percent(95)],
-			1,
-			DisableStrategy::WhenSlashed,
-		);
+			// and we are not forcing a new era
+			assert_eq!(Staking::force_era(), Forcing::NotForcing);
 
-		// the validator doesn't get chilled again
-		assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
+			on_offence_in_era(
+				&[OffenceDetails {
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
+					reporters: vec![],
+				}],
+				// NOTE: A 100% slash here would clean up the account, causing de-registration.
+				&[Perbill::from_percent(95)],
+				1,
+			);
 
-		// but it's disabled
-		assert!(is_disabled(11));
-		// and we are still forcing a new era
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
-	});
+			// the validator doesn't get disabled again
+			assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
+			assert!(!is_disabled(11));
+			// and we are still not forcing a new era
+			assert_eq!(Staking::force_era(), Forcing::NotForcing);
+		});
 }
 
 #[test]
@@ -2671,7 +2685,7 @@ fn dont_slash_if_fraction_is_zero() {
 
 		// The validator hasn't been slashed. The new era is not forced.
 		assert_eq!(Balances::free_balance(11), 1000);
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
+		assert_eq!(Staking::force_era(), Forcing::NotForcing);
 	});
 }
 
@@ -2692,7 +2706,7 @@ fn only_slash_for_max_in_era() {
 
 		// The validator has been slashed and has been force-chilled.
 		assert_eq!(Balances::free_balance(11), 500);
-		assert_eq!(Staking::force_era(), Forcing::ForceNew);
+		assert_eq!(Staking::force_era(), Forcing::NotForcing);
 
 		on_offence_now(
 			&[OffenceDetails {
@@ -2833,7 +2847,6 @@ fn slashing_nominators_by_span_max() {
 			}],
 			&[Perbill::from_percent(10)],
 			2,
-			DisableStrategy::WhenSlashed,
 		);
 
 		assert_eq!(Balances::free_balance(11), 900);
@@ -2860,7 +2873,6 @@ fn slashing_nominators_by_span_max() {
 			}],
 			&[Perbill::from_percent(30)],
 			3,
-			DisableStrategy::WhenSlashed,
 		);
 
 		// 11 was not further slashed, but 21 and 101 were.
@@ -2882,7 +2894,6 @@ fn slashing_nominators_by_span_max() {
 			}],
 			&[Perbill::from_percent(20)],
 			2,
-			DisableStrategy::WhenSlashed,
 		);
 
 		// 11 was further slashed, but 21 and 101 were not.
@@ -2999,11 +3010,8 @@ fn deferred_slashes_are_deferred() {
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::Chilled { stash: 11 },
-				Event::ForceEra { mode: Forcing::ForceNew },
 				Event::SlashReported { validator: 11, slash_era: 1, .. },
 				Event::StakersElected,
-				Event::ForceEra { mode: Forcing::NotForcing },
 				..,
 				Event::Slashed { staker: 11, amount: 100 },
 				Event::Slashed { staker: 101, amount: 12 }
@@ -3029,7 +3037,6 @@ fn retroactive_deferred_slashes_two_eras_before() {
 			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
 			&[Perbill::from_percent(10)],
 			1, // should be deferred for two full eras, and applied at the beginning of era 4.
-			DisableStrategy::Never,
 		);
 
 		mock::start_active_era(4);
@@ -3037,8 +3044,6 @@ fn retroactive_deferred_slashes_two_eras_before() {
 		assert!(matches!(
 			staking_events_since_last_call().as_slice(),
 			&[
-				Event::Chilled { stash: 11 },
-				Event::ForceEra { mode: Forcing::ForceNew },
 				Event::SlashReported { validator: 11, slash_era: 1, .. },
 				..,
 				Event::Slashed { staker: 11, amount: 100 },
@@ -3067,7 +3072,6 @@ fn retroactive_deferred_slashes_one_before() {
 			&[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }],
 			&[Perbill::from_percent(10)],
 			2, // should be deferred for two full eras, and applied at the beginning of era 5.
-			DisableStrategy::Never,
 		);
 
 		mock::start_active_era(4);
@@ -3197,7 +3201,6 @@ fn remove_deferred() {
 			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
 			&[Perbill::from_percent(15)],
 			1,
-			DisableStrategy::WhenSlashed,
 		);
 
 		// fails if empty
@@ -3312,192 +3315,198 @@ fn remove_multi_deferred() {
 
 #[test]
 fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_validator() {
-	ExtBuilder::default().build_and_execute(|| {
-		mock::start_active_era(1);
-		assert_eq_uvec!(Session::validators(), vec![11, 21]);
-
-		// pre-slash balance
-		assert_eq!(Balances::free_balance(11), 1000);
-		assert_eq!(Balances::free_balance(101), 2000);
+	ExtBuilder::default()
+		.validator_count(7)
+		.set_status(41, StakerStatus::Validator)
+		.set_status(51, StakerStatus::Validator)
+		.set_status(201, StakerStatus::Validator)
+		.set_status(202, StakerStatus::Validator)
+		.build_and_execute(|| {
+			mock::start_active_era(1);
+			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]);
 
-		// 100 has approval for 11 as of now
-		assert!(Staking::nominators(101).unwrap().targets.contains(&11));
+			// pre-slash balance
+			assert_eq!(Balances::free_balance(11), 1000);
+			assert_eq!(Balances::free_balance(101), 2000);
 
-		// 11 and 21 both have the support of 100
-		let exposure_11 = Staking::eras_stakers(active_era(), &11);
-		let exposure_21 = Staking::eras_stakers(active_era(), &21);
+			// 100 has approval for 11 as of now
+			assert!(Staking::nominators(101).unwrap().targets.contains(&11));
 
-		assert_eq!(exposure_11.total, 1000 + 125);
-		assert_eq!(exposure_21.total, 1000 + 375);
+			// 11 and 21 both have the support of 100
+			let exposure_11 = Staking::eras_stakers(active_era(), &11);
+			let exposure_21 = Staking::eras_stakers(active_era(), &21);
 
-		on_offence_now(
-			&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(10)],
-		);
+			assert_eq!(exposure_11.total, 1000 + 125);
+			assert_eq!(exposure_21.total, 1000 + 375);
 
-		assert_eq!(
-			staking_events_since_last_call(),
-			vec![
-				Event::StakersElected,
-				Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-				Event::Chilled { stash: 11 },
-				Event::ForceEra { mode: Forcing::ForceNew },
-				Event::SlashReported {
-					validator: 11,
-					fraction: Perbill::from_percent(10),
-					slash_era: 1
-				},
-				Event::Slashed { staker: 11, amount: 100 },
-				Event::Slashed { staker: 101, amount: 12 },
-			]
-		);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(10)],
+			);
 
-		// post-slash balance
-		let nominator_slash_amount_11 = 125 / 10;
-		assert_eq!(Balances::free_balance(11), 900);
-		assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11);
+			assert_eq!(
+				staking_events_since_last_call(),
+				vec![
+					Event::StakersElected,
+					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
+					Event::SlashReported {
+						validator: 11,
+						fraction: Perbill::from_percent(10),
+						slash_era: 1
+					},
+					Event::Slashed { staker: 11, amount: 100 },
+					Event::Slashed { staker: 101, amount: 12 },
+				]
+			);
 
-		// check that validator was chilled.
-		assert!(Validators::<Test>::iter().all(|(stash, _)| stash != 11));
+			// post-slash balance
+			let nominator_slash_amount_11 = 125 / 10;
+			assert_eq!(Balances::free_balance(11), 900);
+			assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11);
 
-		// actually re-bond the slashed validator
-		assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default()));
+			// check that validator was disabled.
+			assert!(is_disabled(11));
 
-		mock::start_active_era(2);
-		let exposure_11 = Staking::eras_stakers(active_era(), &11);
-		let exposure_21 = Staking::eras_stakers(active_era(), &21);
+			// actually re-bond the slashed validator
+			assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default()));
 
-		// 11's own expo is reduced. sum of support from 11 is less (448), which is 500
-		// 900 + 146
-		assert!(matches!(exposure_11, Exposure { own: 900, total: 1046, .. }));
-		// 1000 + 342
-		assert!(matches!(exposure_21, Exposure { own: 1000, total: 1342, .. }));
-		assert_eq!(500 - 146 - 342, nominator_slash_amount_11);
-	});
+			mock::start_active_era(2);
+			let exposure_11 = Staking::eras_stakers(active_era(), &11);
+			let exposure_21 = Staking::eras_stakers(active_era(), &21);
+
+			// 11's own expo is reduced. sum of support from 11 is less (448), which is 500
+			// 900 + 146
+			assert!(matches!(exposure_11, Exposure { own: 900, total: 1046, .. }));
+			// 1000 + 342
+			assert!(matches!(exposure_21, Exposure { own: 1000, total: 1342, .. }));
+			assert_eq!(500 - 146 - 342, nominator_slash_amount_11);
+		});
 }
 
 #[test]
-fn non_slashable_offence_doesnt_disable_validator() {
-	ExtBuilder::default().build_and_execute(|| {
-		mock::start_active_era(1);
-		assert_eq_uvec!(Session::validators(), vec![11, 21]);
+fn non_slashable_offence_disables_validator() {
+	ExtBuilder::default()
+		.validator_count(7)
+		.set_status(41, StakerStatus::Validator)
+		.set_status(51, StakerStatus::Validator)
+		.set_status(201, StakerStatus::Validator)
+		.set_status(202, StakerStatus::Validator)
+		.build_and_execute(|| {
+			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_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
 
-		// offence with no slash associated
-		on_offence_now(
-			&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-			&[Perbill::zero()],
-		);
+			// offence with no slash associated
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::zero()],
+			);
 
-		// it does NOT affect the nominator.
-		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
+			// it does NOT affect the nominator.
+			assert_eq!(Staking::nominators(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)],
-		);
+			// offence that slashes 25% of the bond
+			on_offence_now(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(25)],
+			);
 
-		// it DOES NOT affect the nominator.
-		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
+			// it DOES NOT affect the nominator.
+			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
 
-		assert_eq!(
-			staking_events_since_last_call(),
-			vec![
-				Event::StakersElected,
-				Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-				Event::Chilled { stash: 11 },
-				Event::ForceEra { mode: Forcing::ForceNew },
-				Event::SlashReported {
-					validator: 11,
-					fraction: Perbill::from_percent(0),
-					slash_era: 1
-				},
-				Event::Chilled { stash: 21 },
-				Event::SlashReported {
-					validator: 21,
-					fraction: Perbill::from_percent(25),
-					slash_era: 1
-				},
-				Event::Slashed { staker: 21, amount: 250 },
-				Event::Slashed { staker: 101, amount: 94 }
-			]
-		);
+			assert_eq!(
+				staking_events_since_last_call(),
+				vec![
+					Event::StakersElected,
+					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
+					Event::SlashReported {
+						validator: 11,
+						fraction: Perbill::from_percent(0),
+						slash_era: 1
+					},
+					Event::SlashReported {
+						validator: 21,
+						fraction: Perbill::from_percent(25),
+						slash_era: 1
+					},
+					Event::Slashed { staker: 21, amount: 250 },
+					Event::Slashed { staker: 101, amount: 94 }
+				]
+			);
 
-		// the offence for validator 10 wasn't slashable so it wasn't disabled
-		assert!(!is_disabled(11));
-		// whereas validator 20 gets disabled
-		assert!(is_disabled(21));
-	});
+			// the offence for validator 11 wasn't slashable but it is disabled
+			assert!(is_disabled(11));
+			// validator 21 gets disabled too
+			assert!(is_disabled(21));
+		});
 }
 
 #[test]
 fn slashing_independent_of_disabling_validator() {
-	ExtBuilder::default().build_and_execute(|| {
-		mock::start_active_era(1);
-		assert_eq_uvec!(Session::validators(), vec![11, 21]);
+	ExtBuilder::default()
+		.validator_count(5)
+		.set_status(41, StakerStatus::Validator)
+		.set_status(51, StakerStatus::Validator)
+		.build_and_execute(|| {
+			mock::start_active_era(1);
+			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51]);
 
-		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_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11);
+			let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
 
-		let now = Staking::active_era().unwrap().index;
+			let now = Staking::active_era().unwrap().index;
 
-		// offence with no slash associated, BUT disabling
-		on_offence_in_era(
-			&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-			&[Perbill::zero()],
-			now,
-			DisableStrategy::Always,
-		);
+			// offence with no slash associated
+			on_offence_in_era(
+				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
+				&[Perbill::zero()],
+				now,
+			);
 
-		// nomination remains untouched.
-		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
+			// nomination remains untouched.
+			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
 
-		// offence that slashes 25% of the bond, BUT not disabling
-		on_offence_in_era(
-			&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(25)],
-			now,
-			DisableStrategy::Never,
-		);
+			// offence that slashes 25% of the bond
+			on_offence_in_era(
+				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(25)],
+				now,
+			);
 
-		// nomination remains untouched.
-		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
+			// nomination remains untouched.
+			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
 
-		assert_eq!(
-			staking_events_since_last_call(),
-			vec![
-				Event::StakersElected,
-				Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
-				Event::Chilled { stash: 11 },
-				Event::ForceEra { mode: Forcing::ForceNew },
-				Event::SlashReported {
-					validator: 11,
-					fraction: Perbill::from_percent(0),
-					slash_era: 1
-				},
-				Event::Chilled { stash: 21 },
-				Event::SlashReported {
-					validator: 21,
-					fraction: Perbill::from_percent(25),
-					slash_era: 1
-				},
-				Event::Slashed { staker: 21, amount: 250 },
-				Event::Slashed { staker: 101, amount: 94 }
-			]
-		);
+			assert_eq!(
+				staking_events_since_last_call(),
+				vec![
+					Event::StakersElected,
+					Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 },
+					Event::SlashReported {
+						validator: 11,
+						fraction: Perbill::from_percent(0),
+						slash_era: 1
+					},
+					Event::SlashReported {
+						validator: 21,
+						fraction: Perbill::from_percent(25),
+						slash_era: 1
+					},
+					Event::Slashed { staker: 21, amount: 250 },
+					Event::Slashed { staker: 101, amount: 94 }
+				]
+			);
 
-		// the offence for validator 10 was explicitly disabled
-		assert!(is_disabled(11));
-		// whereas validator 21 is explicitly not disabled
-		assert!(!is_disabled(21));
-	});
+			// first validator is disabled but not slashed
+			assert!(is_disabled(11));
+			// second validator is slashed but not disabled
+			assert!(!is_disabled(21));
+		});
 }
 
 #[test]
-fn offence_threshold_triggers_new_era() {
+fn offence_threshold_doesnt_trigger_new_era() {
 	ExtBuilder::default()
 		.validator_count(4)
 		.set_status(41, StakerStatus::Validator)
@@ -3506,12 +3515,14 @@ fn offence_threshold_triggers_new_era() {
 			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]);
 
 			assert_eq!(
-				<Test as Config>::OffendingValidatorsThreshold::get(),
-				Perbill::from_percent(75),
+				UpToLimitDisablingStrategy::<DISABLING_LIMIT_FACTOR>::disable_limit(
+					Session::validators().len()
+				),
+				1
 			);
 
-			// we have 4 validators and an offending validator threshold of 75%,
-			// once the third validator commits an offence a new era should be forced
+			// 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(Staking::active_era().unwrap().index, &11);
 			let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
@@ -3522,6 +3533,9 @@ fn offence_threshold_triggers_new_era() {
 				&[Perbill::zero()],
 			);
 
+			// 11 should be disabled because the byzantine threshold is 1
+			assert!(is_disabled(11));
+
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
 			on_offence_now(
@@ -3529,6 +3543,10 @@ fn offence_threshold_triggers_new_era() {
 				&[Perbill::zero()],
 			);
 
+			// 21 should not be disabled because the number of disabled validators will be above the
+			// byzantine threshold
+			assert!(!is_disabled(21));
+
 			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 
 			on_offence_now(
@@ -3536,28 +3554,29 @@ fn offence_threshold_triggers_new_era() {
 				&[Perbill::zero()],
 			);
 
-			assert_eq!(ForceEra::<Test>::get(), Forcing::ForceNew);
+			// same for 31
+			assert!(!is_disabled(31));
+
+			assert_eq!(ForceEra::<Test>::get(), Forcing::NotForcing);
 		});
 }
 
 #[test]
 fn disabled_validators_are_kept_disabled_for_whole_era() {
 	ExtBuilder::default()
-		.validator_count(4)
+		.validator_count(7)
 		.set_status(41, StakerStatus::Validator)
+		.set_status(51, StakerStatus::Validator)
+		.set_status(201, StakerStatus::Validator)
+		.set_status(202, StakerStatus::Validator)
 		.build_and_execute(|| {
 			mock::start_active_era(1);
-			assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]);
+			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(Staking::active_era().unwrap().index, &11);
 			let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21);
 
-			on_offence_now(
-				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
-				&[Perbill::zero()],
-			);
-
 			on_offence_now(
 				&[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }],
 				&[Perbill::from_percent(25)],
@@ -3566,18 +3585,15 @@ fn disabled_validators_are_kept_disabled_for_whole_era() {
 			// nominations are not updated.
 			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
 
-			// validator 11 should not be disabled since the offence wasn't slashable
-			assert!(!is_disabled(11));
 			// validator 21 gets disabled since it got slashed
 			assert!(is_disabled(21));
 
 			advance_session();
 
 			// disabled validators should carry-on through all sessions in the era
-			assert!(!is_disabled(11));
 			assert!(is_disabled(21));
 
-			// validator 11 should now get disabled
+			// validator 11 commits an offence
 			on_offence_now(
 				&[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }],
 				&[Perbill::from_percent(25)],
@@ -3687,27 +3703,34 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
 
 #[test]
 fn zero_slash_keeps_nominators() {
-	ExtBuilder::default().build_and_execute(|| {
-		mock::start_active_era(1);
+	ExtBuilder::default()
+		.validator_count(7)
+		.set_status(41, StakerStatus::Validator)
+		.set_status(51, StakerStatus::Validator)
+		.set_status(201, StakerStatus::Validator)
+		.set_status(202, StakerStatus::Validator)
+		.build_and_execute(|| {
+			mock::start_active_era(1);
 
-		assert_eq!(Balances::free_balance(11), 1000);
+			assert_eq!(Balances::free_balance(11), 1000);
 
-		let exposure = Staking::eras_stakers(active_era(), &11);
-		assert_eq!(Balances::free_balance(101), 2000);
+			let exposure = Staking::eras_stakers(active_era(), &11);
+			assert_eq!(Balances::free_balance(101), 2000);
 
-		on_offence_now(
-			&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
-			&[Perbill::from_percent(0)],
-		);
+			on_offence_now(
+				&[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }],
+				&[Perbill::from_percent(0)],
+			);
 
-		assert_eq!(Balances::free_balance(11), 1000);
-		assert_eq!(Balances::free_balance(101), 2000);
+			assert_eq!(Balances::free_balance(11), 1000);
+			assert_eq!(Balances::free_balance(101), 2000);
 
-		// 11 is still removed..
-		assert!(Validators::<Test>::iter().all(|(stash, _)| stash != 11));
-		// but their nominations are kept.
-		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
-	});
+			// 11 is not removed but disabled
+			assert!(Validators::<Test>::iter().any(|(stash, _)| stash == 11));
+			assert!(is_disabled(11));
+			// and their nominations are kept.
+			assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
+		});
 }
 
 #[test]
@@ -4710,7 +4733,7 @@ fn offences_weight_calculated_correctly() {
 		let zero_offence_weight =
 			<Test as frame_system::Config>::DbWeight::get().reads_writes(4, 1);
 		assert_eq!(
-			Staking::on_offence(&[], &[Perbill::from_percent(50)], 0, DisableStrategy::WhenSlashed),
+			Staking::on_offence(&[], &[Perbill::from_percent(50)], 0),
 			zero_offence_weight
 		);
 
@@ -4735,7 +4758,6 @@ fn offences_weight_calculated_correctly() {
 				&offenders,
 				&[Perbill::from_percent(50)],
 				0,
-				DisableStrategy::WhenSlashed
 			),
 			n_offence_unapplied_weight
 		);
@@ -4765,7 +4787,6 @@ fn offences_weight_calculated_correctly() {
 				&one_offender,
 				&[Perbill::from_percent(50)],
 				0,
-				DisableStrategy::WhenSlashed{}
 			),
 			one_offence_unapplied_weight
 		);
@@ -7011,62 +7032,71 @@ mod staking_unchecked {
 
 	#[test]
 	fn virtual_nominators_are_lazily_slashed() {
-		ExtBuilder::default().build_and_execute(|| {
-			mock::start_active_era(1);
-			let slash_percent = Perbill::from_percent(5);
-			let initial_exposure = Staking::eras_stakers(active_era(), &11);
-			// 101 is a nominator for 11
-			assert_eq!(initial_exposure.others.first().unwrap().who, 101);
-			// make 101 a virtual nominator
-			<Staking as StakingUnchecked>::migrate_to_virtual_staker(&101);
-			// set payee different to self.
-			assert_ok!(<Staking as StakingInterface>::update_payee(&101, &102));
-
-			// cache values
-			let nominator_stake = Staking::ledger(101.into()).unwrap().active;
-			let nominator_balance = balances(&101).0;
-			let validator_stake = Staking::ledger(11.into()).unwrap().active;
-			let validator_balance = balances(&11).0;
-			let exposed_stake = initial_exposure.total;
-			let exposed_validator = initial_exposure.own;
-			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],
-			);
+		ExtBuilder::default()
+			.validator_count(7)
+			.set_status(41, StakerStatus::Validator)
+			.set_status(51, StakerStatus::Validator)
+			.set_status(201, StakerStatus::Validator)
+			.set_status(202, StakerStatus::Validator)
+			.build_and_execute(|| {
+				mock::start_active_era(1);
+				let slash_percent = Perbill::from_percent(5);
+				let initial_exposure = Staking::eras_stakers(active_era(), &11);
+				// 101 is a nominator for 11
+				assert_eq!(initial_exposure.others.first().unwrap().who, 101);
+				// make 101 a virtual nominator
+				<Staking as StakingUnchecked>::migrate_to_virtual_staker(&101);
+				// set payee different to self.
+				assert_ok!(<Staking as StakingInterface>::update_payee(&101, &102));
+
+				// cache values
+				let nominator_stake = Staking::ledger(101.into()).unwrap().active;
+				let nominator_balance = balances(&101).0;
+				let validator_stake = Staking::ledger(11.into()).unwrap().active;
+				let validator_balance = balances(&11).0;
+				let exposed_stake = initial_exposure.total;
+				let exposed_validator = initial_exposure.own;
+				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],
+				);
 
-			let slash_amount = slash_percent * exposed_stake;
-			let validator_share =
-				Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
-			let nominator_share =
-				Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
+				let slash_amount = slash_percent * exposed_stake;
+				let validator_share =
+					Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
+				let nominator_share =
+					Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
 
-			// both slash amounts need to be positive for the test to make sense.
-			assert!(validator_share > 0);
-			assert!(nominator_share > 0);
+				// both slash amounts need to be positive for the test to make sense.
+				assert!(validator_share > 0);
+				assert!(nominator_share > 0);
 
-			// both stakes must have been decreased pro-rata.
-			assert_eq!(
-				Staking::ledger(101.into()).unwrap().active,
-				nominator_stake - nominator_share
-			);
-			assert_eq!(
-				Staking::ledger(11.into()).unwrap().active,
-				validator_stake - validator_share
-			);
+				// both stakes must have been decreased pro-rata.
+				assert_eq!(
+					Staking::ledger(101.into()).unwrap().active,
+					nominator_stake - nominator_share
+				);
+				assert_eq!(
+					Staking::ledger(11.into()).unwrap().active,
+					validator_stake - validator_share
+				);
 
-			// validator balance is slashed as usual
-			assert_eq!(balances(&11).0, validator_balance - validator_share);
-			// Because slashing happened.
-			assert!(is_disabled(11));
+				// validator balance is slashed as usual
+				assert_eq!(balances(&11).0, validator_balance - validator_share);
+				// Because slashing happened.
+				assert!(is_disabled(11));
 
-			// but virtual nominator's balance is not slashed.
-			assert_eq!(Balances::free_balance(&101), nominator_balance);
-			// but slash is broadcasted to slash observers.
-			assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share);
-		})
+				// but virtual nominator's balance is not slashed.
+				assert_eq!(Balances::free_balance(&101), nominator_balance);
+				// but slash is broadcasted to slash observers.
+				assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share);
+			})
 	}
 }
 mod ledger {
@@ -7926,3 +7956,69 @@ mod ledger_recovery {
 		})
 	}
 }
+
+mod byzantine_threshold_disabling_strategy {
+	use crate::{
+		tests::Test, ActiveEra, ActiveEraInfo, DisablingStrategy, UpToLimitDisablingStrategy,
+	};
+	use sp_staking::EraIndex;
+
+	// Common test data - the stash of the offending validator, the era of the offence and the
+	// active set
+	const OFFENDER_ID: <Test as frame_system::Config>::AccountId = 7;
+	const SLASH_ERA: EraIndex = 1;
+	const ACTIVE_SET: [<Test as pallet_session::Config>::ValidatorId; 7] = [1, 2, 3, 4, 5, 6, 7];
+	const OFFENDER_VALIDATOR_IDX: u32 = 6; // the offender is with index 6 in the active set
+
+	#[test]
+	fn dont_disable_for_ancient_offence() {
+		sp_io::TestExternalities::default().execute_with(|| {
+			let initially_disabled = vec![];
+			pallet_session::Validators::<Test>::put(ACTIVE_SET.to_vec());
+			ActiveEra::<Test>::put(ActiveEraInfo { index: 2, start: None });
+
+			let disable_offender =
+				<UpToLimitDisablingStrategy as DisablingStrategy<Test>>::decision(
+					&OFFENDER_ID,
+					SLASH_ERA,
+					&initially_disabled,
+				);
+
+			assert!(disable_offender.is_none());
+		});
+	}
+
+	#[test]
+	fn dont_disable_beyond_byzantine_threshold() {
+		sp_io::TestExternalities::default().execute_with(|| {
+			let initially_disabled = vec![1, 2];
+			pallet_session::Validators::<Test>::put(ACTIVE_SET.to_vec());
+
+			let disable_offender =
+				<UpToLimitDisablingStrategy as DisablingStrategy<Test>>::decision(
+					&OFFENDER_ID,
+					SLASH_ERA,
+					&initially_disabled,
+				);
+
+			assert!(disable_offender.is_none());
+		});
+	}
+
+	#[test]
+	fn disable_when_below_byzantine_threshold() {
+		sp_io::TestExternalities::default().execute_with(|| {
+			let initially_disabled = vec![1];
+			pallet_session::Validators::<Test>::put(ACTIVE_SET.to_vec());
+
+			let disable_offender =
+				<UpToLimitDisablingStrategy as DisablingStrategy<Test>>::decision(
+					&OFFENDER_ID,
+					SLASH_ERA,
+					&initially_disabled,
+				);
+
+			assert_eq!(disable_offender, Some(OFFENDER_VALIDATOR_IDX));
+		});
+	}
+}
diff --git a/substrate/primitives/staking/src/offence.rs b/substrate/primitives/staking/src/offence.rs
index 30d96d0cbafce9f0540ffbb49cf15c7375ea405f..2c2ebc1fc971f25cff94b0cef4913eca19551e69 100644
--- a/substrate/primitives/staking/src/offence.rs
+++ b/substrate/primitives/staking/src/offence.rs
@@ -37,29 +37,6 @@ pub type Kind = [u8; 16];
 /// so that we can slash it accordingly.
 pub type OffenceCount = u32;
 
-/// In case of an offence, which conditions get an offending validator disabled.
-#[derive(
-	Clone,
-	Copy,
-	PartialEq,
-	Eq,
-	Hash,
-	PartialOrd,
-	Ord,
-	Encode,
-	Decode,
-	sp_runtime::RuntimeDebug,
-	scale_info::TypeInfo,
-)]
-pub enum DisableStrategy {
-	/// Independently of slashing, this offence will not disable the offender.
-	Never,
-	/// Only disable the offender if it is also slashed.
-	WhenSlashed,
-	/// Independently of slashing, this offence will always disable the offender.
-	Always,
-}
-
 /// A trait implemented by an offence report.
 ///
 /// This trait assumes that the offence is legitimate and was validated already.
@@ -102,11 +79,6 @@ pub trait Offence<Offender> {
 	/// number. Note that for GRANDPA the round number is reset each epoch.
 	fn time_slot(&self) -> Self::TimeSlot;
 
-	/// In which cases this offence needs to disable offenders until the next era starts.
-	fn disable_strategy(&self) -> DisableStrategy {
-		DisableStrategy::WhenSlashed
-	}
-
 	/// A slash fraction of the total exposure that should be slashed for this
 	/// particular offence for the `offenders_count` that happened at a singular `TimeSlot`.
 	///
@@ -177,15 +149,12 @@ pub trait OnOffenceHandler<Reporter, Offender, Res> {
 	///
 	/// The `session` parameter is the session index of the offence.
 	///
-	/// The `disable_strategy` parameter decides if the offenders need to be disabled immediately.
-	///
 	/// The receiver might decide to not accept this offence. In this case, the call site is
 	/// responsible for queuing the report and re-submitting again.
 	fn on_offence(
 		offenders: &[OffenceDetails<Reporter, Offender>],
 		slash_fraction: &[Perbill],
 		session: SessionIndex,
-		disable_strategy: DisableStrategy,
 	) -> Res;
 }
 
@@ -194,7 +163,6 @@ impl<Reporter, Offender, Res: Default> OnOffenceHandler<Reporter, Offender, Res>
 		_offenders: &[OffenceDetails<Reporter, Offender>],
 		_slash_fraction: &[Perbill],
 		_session: SessionIndex,
-		_disable_strategy: DisableStrategy,
 	) -> Res {
 		Default::default()
 	}