From b56c0e4cb6013356451d62a8657aebf8de058818 Mon Sep 17 00:00:00 2001
From: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Date: Fri, 23 Sep 2022 10:36:33 +0100
Subject: [PATCH] Fast Unstake Pallet (#12129)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* add failing test for itamar

* an ugly example of fast unstake

* Revert "add failing test for itamar"

This reverts commit 16c4d8015698a0684c090c54fce8b470a2d2feb2.

* fast unstake wip

* clean it up a bit

* some comments

* on_idle logic

* fix

* comment

* new working version, checks all pass, looking good

* some notes

* add mock boilerplate

* more boilerplate

* simplify the weight stuff

* ExtBuilder for pools

* fmt

* rm bags-list, simplify setup_works

* mock + tests boilerplate

* make some benchmarks work

* mock boilerplate

* tests boilerplate

* run_to_block works

* add Error enums

* add test

* note

* make UnstakeRequest fields pub

* some tests

* fix origin

* fmt

* add fast_unstake_events_since_last_call

* text

* rewrite some benchmes and fix them -- the outcome is still strange

* Fix weights

* cleanup

* Update frame/election-provider-support/solution-type/src/single_page.rs

* fix build

* Fix pools tests

* iterate teset + mock

* test unfinished

* cleanup and add some tests

* add test successful_multi_queue

* comment

* rm Head check

* add TODO

* complete successful_multi_queue

* + test early_exit

* fix a lot of things above the beautiful atlantic ocean 🌊

* seemingly it is finished now

* Fix build

* ".git/.scripts/fmt.sh" 1

* Fix slashing amount as well

* better docs

* abstract types

* rm use

* import

* Update frame/nomination-pools/benchmarking/src/lib.rs

Co-authored-by: Nitwit <47109040+nitwit69@users.noreply.github.com>

* Update frame/fast-unstake/src/types.rs

Co-authored-by: Nitwit <47109040+nitwit69@users.noreply.github.com>

* Fix build

* fmt

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>

* make bounded

* feedback from code review with Ankan

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Roman Useinov <roman.useinov@gmail.com>

* Update frame/fast-unstake/src/mock.rs

* update to master

* some final review comments

* fmt

* fix clippy

* remove unused

* ".git/.scripts/fmt.sh" 1

* make it all build again

* fmt

* undo fishy change

Co-authored-by: Ross Bulat <ross@jkrbinvestments.com>
Co-authored-by: command-bot <>
Co-authored-by: Nitwit <47109040+nitwit69@users.noreply.github.com>
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
Co-authored-by: Roman Useinov <roman.useinov@gmail.com>
---
 substrate/Cargo.lock                          |   26 +
 substrate/Cargo.toml                          |    1 +
 substrate/bin/node/runtime/Cargo.toml         |    4 +
 substrate/bin/node/runtime/src/lib.rs         |    9 +
 .../election-provider-multi-phase/src/lib.rs  |   11 +
 .../election-provider-multi-phase/src/mock.rs |    4 +
 .../election-provider-support/src/lib.rs      |    9 +-
 .../election-provider-support/src/onchain.rs  |    8 +
 substrate/frame/fast-unstake/Cargo.toml       |   70 ++
 .../frame/fast-unstake/src/benchmarking.rs    |  228 ++++
 substrate/frame/fast-unstake/src/lib.rs       |  505 ++++++++
 substrate/frame/fast-unstake/src/mock.rs      |  387 ++++++
 substrate/frame/fast-unstake/src/tests.rs     | 1036 +++++++++++++++++
 substrate/frame/fast-unstake/src/types.rs     |  119 ++
 substrate/frame/fast-unstake/src/weights.rs   |  210 ++++
 .../nomination-pools/benchmarking/src/lib.rs  |   36 +-
 substrate/frame/nomination-pools/src/lib.rs   |    3 +-
 substrate/frame/nomination-pools/src/tests.rs |    2 +-
 substrate/frame/staking/src/lib.rs            |    2 +-
 substrate/frame/staking/src/pallet/impls.rs   |    4 +-
 substrate/frame/staking/src/pallet/mod.rs     |    5 +-
 21 files changed, 2650 insertions(+), 29 deletions(-)
 create mode 100644 substrate/frame/fast-unstake/Cargo.toml
 create mode 100644 substrate/frame/fast-unstake/src/benchmarking.rs
 create mode 100644 substrate/frame/fast-unstake/src/lib.rs
 create mode 100644 substrate/frame/fast-unstake/src/mock.rs
 create mode 100644 substrate/frame/fast-unstake/src/tests.rs
 create mode 100644 substrate/frame/fast-unstake/src/types.rs
 create mode 100644 substrate/frame/fast-unstake/src/weights.rs

diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock
index 5172c3d0286..d1349ce488f 100644
--- a/substrate/Cargo.lock
+++ b/substrate/Cargo.lock
@@ -3376,6 +3376,7 @@ dependencies = [
  "pallet-election-provider-multi-phase",
  "pallet-election-provider-support-benchmarking",
  "pallet-elections-phragmen",
+ "pallet-fast-unstake",
  "pallet-gilt",
  "pallet-grandpa",
  "pallet-identity",
@@ -5718,6 +5719,31 @@ dependencies = [
  "sp-tasks",
 ]
 
+[[package]]
+name = "pallet-fast-unstake"
+version = "4.0.0-dev"
+dependencies = [
+ "frame-benchmarking",
+ "frame-election-provider-support",
+ "frame-support",
+ "frame-system",
+ "log",
+ "pallet-balances",
+ "pallet-nomination-pools",
+ "pallet-staking",
+ "pallet-staking-reward-curve",
+ "pallet-timestamp",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-staking",
+ "sp-std",
+ "sp-tracing",
+ "substrate-test-utils",
+]
+
 [[package]]
 name = "pallet-gilt"
 version = "4.0.0-dev"
diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml
index 48a31940fd3..018355df6c9 100644
--- a/substrate/Cargo.toml
+++ b/substrate/Cargo.toml
@@ -90,6 +90,7 @@ members = [
 	"frame/contracts/rpc/runtime-api",
 	"frame/conviction-voting",
 	"frame/democracy",
+	"frame/fast-unstake",
 	"frame/try-runtime",
 	"frame/election-provider-multi-phase",
 	"frame/election-provider-support",
diff --git a/substrate/bin/node/runtime/Cargo.toml b/substrate/bin/node/runtime/Cargo.toml
index d52b1aaccfc..e7220242316 100644
--- a/substrate/bin/node/runtime/Cargo.toml
+++ b/substrate/bin/node/runtime/Cargo.toml
@@ -68,6 +68,7 @@ pallet-democracy = { version = "4.0.0-dev", default-features = false, path = "..
 pallet-election-provider-multi-phase = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-multi-phase" }
 pallet-election-provider-support-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-support/benchmarking", optional = true }
 pallet-elections-phragmen = { version = "5.0.0-dev", default-features = false, path = "../../../frame/elections-phragmen" }
+pallet-fast-unstake = { version = "4.0.0-dev", default-features = false, path = "../../../frame/fast-unstake" }
 pallet-gilt = { version = "4.0.0-dev", default-features = false, path = "../../../frame/gilt" }
 pallet-grandpa = { version = "4.0.0-dev", default-features = false, path = "../../../frame/grandpa" }
 pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" }
@@ -142,6 +143,7 @@ std = [
 	"pallet-conviction-voting/std",
 	"pallet-democracy/std",
 	"pallet-elections-phragmen/std",
+	"pallet-fast-unstake/std",
 	"frame-executive/std",
 	"pallet-gilt/std",
 	"pallet-grandpa/std",
@@ -220,6 +222,7 @@ runtime-benchmarks = [
 	"pallet-election-provider-multi-phase/runtime-benchmarks",
 	"pallet-election-provider-support-benchmarking/runtime-benchmarks",
 	"pallet-elections-phragmen/runtime-benchmarks",
+	"pallet-fast-unstake/runtime-benchmarks",
 	"pallet-gilt/runtime-benchmarks",
 	"pallet-grandpa/runtime-benchmarks",
 	"pallet-identity/runtime-benchmarks",
@@ -272,6 +275,7 @@ try-runtime = [
 	"pallet-democracy/try-runtime",
 	"pallet-election-provider-multi-phase/try-runtime",
 	"pallet-elections-phragmen/try-runtime",
+	"pallet-fast-unstake/try-runtime",
 	"pallet-gilt/try-runtime",
 	"pallet-grandpa/try-runtime",
 	"pallet-im-online/try-runtime",
diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs
index d6a9798b4d9..8ed5f1c847f 100644
--- a/substrate/bin/node/runtime/src/lib.rs
+++ b/substrate/bin/node/runtime/src/lib.rs
@@ -579,6 +579,13 @@ impl pallet_staking::Config for Runtime {
 	type BenchmarkingConfig = StakingBenchmarkingConfig;
 }
 
+impl pallet_fast_unstake::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type SlashPerEra = ConstU128<{ DOLLARS }>;
+	type ControlOrigin = frame_system::EnsureRoot<AccountId>;
+	type WeightInfo = ();
+}
+
 parameter_types! {
 	// phase durations. 1/4 of the last session for each.
 	pub const SignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4;
@@ -1655,6 +1662,7 @@ construct_runtime!(
 		NominationPools: pallet_nomination_pools,
 		RankedPolls: pallet_referenda::<Instance2>,
 		RankedCollective: pallet_ranked_collective,
+		FastUnstake: pallet_fast_unstake,
 	}
 );
 
@@ -1741,6 +1749,7 @@ mod benches {
 		[pallet_election_provider_multi_phase, ElectionProviderMultiPhase]
 		[pallet_election_provider_support_benchmarking, EPSBench::<Runtime>]
 		[pallet_elections_phragmen, Elections]
+		[pallet_fast_unstake, FastUnstake]
 		[pallet_gilt, Gilt]
 		[pallet_grandpa, Grandpa]
 		[pallet_identity, Identity]
diff --git a/substrate/frame/election-provider-multi-phase/src/lib.rs b/substrate/frame/election-provider-multi-phase/src/lib.rs
index 3c9b15dabb0..05353e5a3ac 100644
--- a/substrate/frame/election-provider-multi-phase/src/lib.rs
+++ b/substrate/frame/election-provider-multi-phase/src/lib.rs
@@ -318,6 +318,10 @@ impl<T: Config> ElectionProvider for NoFallback<T> {
 	type DataProvider = T::DataProvider;
 	type Error = &'static str;
 
+	fn ongoing() -> bool {
+		false
+	}
+
 	fn elect() -> Result<Supports<T::AccountId>, Self::Error> {
 		// Do nothing, this will enable the emergency phase.
 		Err("NoFallback.")
@@ -1598,6 +1602,13 @@ impl<T: Config> ElectionProvider for Pallet<T> {
 	type Error = ElectionError<T>;
 	type DataProvider = T::DataProvider;
 
+	fn ongoing() -> bool {
+		match Self::current_phase() {
+			Phase::Off => false,
+			_ => true,
+		}
+	}
+
 	fn elect() -> Result<Supports<T::AccountId>, Self::Error> {
 		match Self::do_elect() {
 			Ok(supports) => {
diff --git a/substrate/frame/election-provider-multi-phase/src/mock.rs b/substrate/frame/election-provider-multi-phase/src/mock.rs
index d7affc14564..34aa2e1bbfc 100644
--- a/substrate/frame/election-provider-multi-phase/src/mock.rs
+++ b/substrate/frame/election-provider-multi-phase/src/mock.rs
@@ -303,6 +303,10 @@ impl ElectionProvider for MockFallback {
 	type Error = &'static str;
 	type DataProvider = StakingMock;
 
+	fn ongoing() -> bool {
+		false
+	}
+
 	fn elect() -> Result<Supports<AccountId>, Self::Error> {
 		Self::elect_with_bounds(Bounded::max_value(), Bounded::max_value())
 	}
diff --git a/substrate/frame/election-provider-support/src/lib.rs b/substrate/frame/election-provider-support/src/lib.rs
index 9f60411435d..0bf62bd8c35 100644
--- a/substrate/frame/election-provider-support/src/lib.rs
+++ b/substrate/frame/election-provider-support/src/lib.rs
@@ -136,7 +136,7 @@
 //!         type BlockNumber = BlockNumber;
 //!         type Error = &'static str;
 //!         type DataProvider = T::DataProvider;
-//!
+//! 	        fn ongoing() -> bool { false }
 //!         fn elect() -> Result<Supports<AccountId>, Self::Error> {
 //!             Self::DataProvider::electable_targets(None)
 //!                 .map_err(|_| "failed to elect")
@@ -370,6 +370,9 @@ pub trait ElectionProvider {
 		BlockNumber = Self::BlockNumber,
 	>;
 
+	/// Indicate if this election provider is currently ongoing an asynchronous election or not.
+	fn ongoing() -> bool;
+
 	/// Elect a new set of winners, without specifying any bounds on the amount of data fetched from
 	/// [`Self::DataProvider`]. An implementation could nonetheless impose its own custom limits.
 	///
@@ -420,6 +423,10 @@ where
 	fn elect() -> Result<Supports<AccountId>, Self::Error> {
 		Err("<NoElection as ElectionProvider> cannot do anything.")
 	}
+
+	fn ongoing() -> bool {
+		false
+	}
 }
 
 /// A utility trait for something to implement `ElectionDataProvider` in a sensible way.
diff --git a/substrate/frame/election-provider-support/src/onchain.rs b/substrate/frame/election-provider-support/src/onchain.rs
index e477499f3c9..10c3519d03d 100644
--- a/substrate/frame/election-provider-support/src/onchain.rs
+++ b/substrate/frame/election-provider-support/src/onchain.rs
@@ -138,6 +138,10 @@ impl<T: Config> ElectionProvider for UnboundedExecution<T> {
 	type Error = Error;
 	type DataProvider = T::DataProvider;
 
+	fn ongoing() -> bool {
+		false
+	}
+
 	fn elect() -> Result<Supports<Self::AccountId>, Self::Error> {
 		// This should not be called if not in `std` mode (and therefore neither in genesis nor in
 		// testing)
@@ -167,6 +171,10 @@ impl<T: BoundedConfig> ElectionProvider for BoundedExecution<T> {
 	type Error = Error;
 	type DataProvider = T::DataProvider;
 
+	fn ongoing() -> bool {
+		false
+	}
+
 	fn elect() -> Result<Supports<Self::AccountId>, Self::Error> {
 		elect_with::<T>(Some(T::VotersBound::get() as usize), Some(T::TargetsBound::get() as usize))
 	}
diff --git a/substrate/frame/fast-unstake/Cargo.toml b/substrate/frame/fast-unstake/Cargo.toml
new file mode 100644
index 00000000000..1fa118dba4a
--- /dev/null
+++ b/substrate/frame/fast-unstake/Cargo.toml
@@ -0,0 +1,70 @@
+[package]
+name = "pallet-fast-unstake"
+version = "4.0.0-dev"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "Unlicense"
+homepage = "https://substrate.io"
+repository = "https://github.com/paritytech/substrate/"
+description = "FRAME fast unstake pallet"
+readme = "README.md"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
+log = { version = "0.4.17", default-features = false }
+scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
+
+frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
+frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
+
+sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" }
+sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" }
+sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" }
+sp-staking = { default-features = false, path = "../../primitives/staking" }
+
+pallet-balances = { default-features = false, path = "../balances" }
+pallet-timestamp = { default-features = false, path = "../timestamp" }
+pallet-staking = { default-features = false, path = "../staking" }
+pallet-nomination-pools = { default-features = false, path = "../nomination-pools" }
+frame-election-provider-support = { default-features = false, path = "../election-provider-support" }
+
+frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" }
+
+[dev-dependencies]
+pallet-staking-reward-curve = { version = "4.0.0-dev", path = "../staking/reward-curve" }
+sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" }
+substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" }
+sp-tracing = { version = "5.0.0", path = "../../primitives/tracing" }
+
+[features]
+default = ["std"]
+std = [
+	"codec/std",
+	"log/std",
+	"scale-info/std",
+
+	"frame-support/std",
+	"frame-system/std",
+
+	"sp-io/std",
+	"sp-staking/std",
+	"sp-runtime/std",
+	"sp-std/std",
+
+	"pallet-staking/std",
+	"pallet-nomination-pools/std",
+	"pallet-balances/std",
+	"pallet-timestamp/std",
+	"frame-election-provider-support/std",
+
+	"frame-benchmarking/std",
+]
+runtime-benchmarks = [
+	"frame-benchmarking/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+	"pallet-staking/runtime-benchmarks",
+]
+try-runtime = ["frame-support/try-runtime"]
diff --git a/substrate/frame/fast-unstake/src/benchmarking.rs b/substrate/frame/fast-unstake/src/benchmarking.rs
new file mode 100644
index 00000000000..68a3da0d40a
--- /dev/null
+++ b/substrate/frame/fast-unstake/src/benchmarking.rs
@@ -0,0 +1,228 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Benchmarking for pallet-fast-unstake.
+
+#![cfg(feature = "runtime-benchmarks")]
+
+use crate::{types::*, Pallet as FastUnstake, *};
+use frame_benchmarking::{benchmarks, whitelist_account};
+use frame_support::{
+	assert_ok,
+	traits::{Currency, EnsureOrigin, Get, Hooks},
+};
+use frame_system::RawOrigin;
+use pallet_nomination_pools::{Pallet as Pools, PoolId};
+use pallet_staking::Pallet as Staking;
+use sp_runtime::traits::{StaticLookup, Zero};
+use sp_staking::EraIndex;
+use sp_std::prelude::*;
+
+const USER_SEED: u32 = 0;
+const DEFAULT_BACKER_PER_VALIDATOR: u32 = 128;
+const MAX_VALIDATORS: u32 = 128;
+
+type CurrencyOf<T> = <T as pallet_staking::Config>::Currency;
+
+fn l<T: Config>(
+	who: T::AccountId,
+) -> <<T as frame_system::Config>::Lookup as StaticLookup>::Source {
+	T::Lookup::unlookup(who)
+}
+
+fn create_unexposed_nominator<T: Config>() -> T::AccountId {
+	let account = frame_benchmarking::account::<T::AccountId>("nominator_42", 0, USER_SEED);
+	fund_and_bond_account::<T>(&account);
+	account
+}
+
+fn fund_and_bond_account<T: Config>(account: &T::AccountId) {
+	let stake = CurrencyOf::<T>::minimum_balance() * 100u32.into();
+	CurrencyOf::<T>::make_free_balance_be(&account, stake * 10u32.into());
+
+	let account_lookup = l::<T>(account.clone());
+	// bond and nominate ourselves, this will guarantee that we are not backing anyone.
+	assert_ok!(Staking::<T>::bond(
+		RawOrigin::Signed(account.clone()).into(),
+		account_lookup.clone(),
+		stake,
+		pallet_staking::RewardDestination::Controller,
+	));
+	assert_ok!(Staking::<T>::nominate(
+		RawOrigin::Signed(account.clone()).into(),
+		vec![account_lookup]
+	));
+}
+
+pub(crate) fn fast_unstake_events<T: Config>() -> Vec<crate::Event<T>> {
+	frame_system::Pallet::<T>::events()
+		.into_iter()
+		.map(|r| r.event)
+		.filter_map(|e| <T as Config>::RuntimeEvent::from(e).try_into().ok())
+		.collect::<Vec<_>>()
+}
+
+fn setup_pool<T: Config>() -> PoolId {
+	let depositor = frame_benchmarking::account::<T::AccountId>("depositor_42", 0, USER_SEED);
+	let depositor_lookup = l::<T>(depositor.clone());
+
+	let stake = Pools::<T>::depositor_min_bond();
+	CurrencyOf::<T>::make_free_balance_be(&depositor, stake * 10u32.into());
+
+	Pools::<T>::create(
+		RawOrigin::Signed(depositor.clone()).into(),
+		stake,
+		depositor_lookup.clone(),
+		depositor_lookup.clone(),
+		depositor_lookup,
+	)
+	.unwrap();
+
+	pallet_nomination_pools::LastPoolId::<T>::get()
+}
+
+fn setup_staking<T: Config>(v: u32, until: EraIndex) {
+	let ed = CurrencyOf::<T>::minimum_balance();
+
+	log!(debug, "registering {} validators and {} eras.", v, until);
+
+	// our validators don't actually need to registered in staking -- just generate `v` random
+	// accounts.
+	let validators = (0..v)
+		.map(|x| frame_benchmarking::account::<T::AccountId>("validator", x, USER_SEED))
+		.collect::<Vec<_>>();
+
+	for era in 0..=until {
+		let others = (0..DEFAULT_BACKER_PER_VALIDATOR)
+			.map(|s| {
+				let who = frame_benchmarking::account::<T::AccountId>("nominator", era, s);
+				let value = ed;
+				pallet_staking::IndividualExposure { who, value }
+			})
+			.collect::<Vec<_>>();
+		let exposure =
+			pallet_staking::Exposure { total: Default::default(), own: Default::default(), others };
+		validators.iter().for_each(|v| {
+			Staking::<T>::add_era_stakers(era, v.clone(), exposure.clone());
+		});
+	}
+}
+
+fn on_idle_full_block<T: Config>() {
+	let remaining_weight = <T as frame_system::Config>::BlockWeights::get().max_block;
+	FastUnstake::<T>::on_idle(Zero::zero(), remaining_weight);
+}
+
+benchmarks! {
+	// on_idle, we we don't check anyone, but fully unbond and move them to another pool.
+	on_idle_unstake {
+		let who = create_unexposed_nominator::<T>();
+		let pool_id = setup_pool::<T>();
+		assert_ok!(FastUnstake::<T>::register_fast_unstake(
+			RawOrigin::Signed(who.clone()).into(),
+			Some(pool_id)
+		));
+		ErasToCheckPerBlock::<T>::put(1);
+
+		// run on_idle once. This will check era 0.
+		assert_eq!(Head::<T>::get(), None);
+		on_idle_full_block::<T>();
+		assert_eq!(
+			Head::<T>::get(),
+			Some(UnstakeRequest { stash: who.clone(), checked: vec![0].try_into().unwrap(), maybe_pool_id: Some(pool_id) })
+		);
+	}
+	: {
+		on_idle_full_block::<T>();
+	}
+	verify {
+		assert!(matches!(
+			fast_unstake_events::<T>().last(),
+			Some(Event::Unstaked { .. })
+		));
+	}
+
+	// on_idle, when we check some number of eras,
+	on_idle_check {
+		// number of eras multiplied by validators in that era.
+		let x in (<T as pallet_staking::Config>::BondingDuration::get() * 1) .. (<T as pallet_staking::Config>::BondingDuration::get() * MAX_VALIDATORS);
+
+		let v = x / <T as pallet_staking::Config>::BondingDuration::get();
+		let u = <T as pallet_staking::Config>::BondingDuration::get();
+
+		ErasToCheckPerBlock::<T>::put(u);
+		pallet_staking::CurrentEra::<T>::put(u);
+
+		// setup staking with v validators and u eras of data (0..=u)
+		setup_staking::<T>(v, u);
+		let who = create_unexposed_nominator::<T>();
+		assert_ok!(FastUnstake::<T>::register_fast_unstake(
+			RawOrigin::Signed(who.clone()).into(),
+			None,
+		));
+
+		// no one is queued thus far.
+		assert_eq!(Head::<T>::get(), None);
+	}
+	: {
+		on_idle_full_block::<T>();
+	}
+	verify {
+		let checked: frame_support::BoundedVec<_, _> = (1..=u).rev().collect::<Vec<EraIndex>>().try_into().unwrap();
+		assert_eq!(
+			Head::<T>::get(),
+			Some(UnstakeRequest { stash: who.clone(), checked, maybe_pool_id: None })
+		);
+		assert!(matches!(
+			fast_unstake_events::<T>().last(),
+			Some(Event::Checking { .. })
+		));
+	}
+
+	register_fast_unstake {
+		let who = create_unexposed_nominator::<T>();
+		whitelist_account!(who);
+		assert_eq!(Queue::<T>::count(), 0);
+
+	}
+	:_(RawOrigin::Signed(who.clone()), None)
+	verify {
+		assert_eq!(Queue::<T>::count(), 1);
+	}
+
+	deregister {
+		let who = create_unexposed_nominator::<T>();
+		assert_ok!(FastUnstake::<T>::register_fast_unstake(
+			RawOrigin::Signed(who.clone()).into(),
+			None
+		));
+		assert_eq!(Queue::<T>::count(), 1);
+		whitelist_account!(who);
+	}
+	:_(RawOrigin::Signed(who.clone()))
+	verify {
+		assert_eq!(Queue::<T>::count(), 0);
+	}
+
+	control {
+		let origin = <T as Config>::ControlOrigin::successful_origin();
+	}
+	: _<T::RuntimeOrigin>(origin, 128)
+	verify {}
+
+	impl_benchmark_test_suite!(Pallet, crate::mock::ExtBuilder::default().build(), crate::mock::Runtime)
+}
diff --git a/substrate/frame/fast-unstake/src/lib.rs b/substrate/frame/fast-unstake/src/lib.rs
new file mode 100644
index 00000000000..51416808f48
--- /dev/null
+++ b/substrate/frame/fast-unstake/src/lib.rs
@@ -0,0 +1,505 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! A pallet that's designed to JUST do the following:
+//!
+//! If a nominator is not exposed in any `ErasStakers` (i.e. "has not actively backed any
+//! validators in the last `BondingDuration` days"), then they can register themselves in this
+//! pallet, unstake faster than having to wait an entire bonding duration, and potentially move
+//! into a nomination pool.
+//!
+//! Appearing in the exposure of a validator means being exposed equal to that validator from the
+//! point of view of the staking system. This usually means earning rewards with the validator, and
+//! also being at the risk of slashing with the validator. This is equivalent to the "Active
+//! Nominator" role explained in the
+//! [February Staking Update](https://polkadot.network/blog/staking-update-february-2022/).
+//!
+//! This pallet works off the basis of `on_idle`, meaning that it provides no guarantee about when
+//! it will succeed, if at all. Moreover, the queue implementation is unordered. In case of
+//! congestion, no FIFO ordering is provided.
+//!
+//! Stakers who are certain about NOT being exposed can register themselves with
+//! [`Call::register_fast_unstake`]. This will chill, and fully unbond the staker, and place them in
+//! the queue to be checked.
+//!
+//! Once queued, but not being actively processed, stakers can withdraw their request via
+//! [`Call::deregister`].
+//!
+//! Once queued, a staker wishing to unbond can perform no further action in pallet-staking. This is
+//! to prevent them from accidentally exposing themselves behind a validator etc.
+//!
+//! Once processed, if successful, no additional fee for the checking process is taken, and the
+//! staker is instantly unbonded. Optionally, if they have asked to join a pool, their *entire*
+//! stake is joined into their pool of choice.
+//!
+//! If unsuccessful, meaning that the staker was exposed sometime in the last `BondingDuration` eras
+//! they will end up being slashed for the amount of wasted work they have inflicted on the chian.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub use pallet::*;
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+// NOTE: enable benchmarking in tests as well.
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+mod types;
+pub mod weights;
+
+pub const LOG_TARGET: &'static str = "runtime::fast-unstake";
+
+// syntactic sugar for logging.
+#[macro_export]
+macro_rules! log {
+	($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
+		log::$level!(
+			target: crate::LOG_TARGET,
+			concat!("[{:?}] 💨 ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
+		)
+	};
+}
+
+#[frame_support::pallet]
+pub mod pallet {
+	use super::*;
+	use crate::types::*;
+	use frame_election_provider_support::ElectionProvider;
+	use frame_support::pallet_prelude::*;
+	use frame_system::{pallet_prelude::*, RawOrigin};
+	use pallet_nomination_pools::PoolId;
+	use pallet_staking::Pallet as Staking;
+	use sp_runtime::{
+		traits::{Saturating, Zero},
+		DispatchResult,
+	};
+	use sp_staking::EraIndex;
+	use sp_std::{prelude::*, vec::Vec};
+	use weights::WeightInfo;
+
+	#[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)]
+	#[codec(mel_bound(T: Config))]
+	#[scale_info(skip_type_params(T))]
+	pub struct MaxChecking<T: Config>(sp_std::marker::PhantomData<T>);
+	impl<T: Config> frame_support::traits::Get<u32> for MaxChecking<T> {
+		fn get() -> u32 {
+			<T as pallet_staking::Config>::BondingDuration::get() + 1
+		}
+	}
+
+	#[pallet::pallet]
+	pub struct Pallet<T>(_);
+
+	#[pallet::config]
+	pub trait Config:
+		frame_system::Config
+		+ pallet_staking::Config<
+			CurrencyBalance = <Self as pallet_nomination_pools::Config>::CurrencyBalance,
+		> + pallet_nomination_pools::Config
+	{
+		/// The overarching event type.
+		type RuntimeEvent: From<Event<Self>>
+			+ IsType<<Self as frame_system::Config>::RuntimeEvent>
+			+ TryInto<Event<Self>>;
+
+		/// The amount of balance slashed per each era that was wastefully checked.
+		///
+		/// A reasonable value could be `runtime_weight_to_fee(weight_per_era_check)`.
+		type SlashPerEra: Get<BalanceOf<Self>>;
+
+		/// The origin that can control this pallet.
+		type ControlOrigin: frame_support::traits::EnsureOrigin<Self::RuntimeOrigin>;
+
+		/// The weight information of this pallet.
+		type WeightInfo: WeightInfo;
+	}
+
+	/// The current "head of the queue" being unstaked.
+	#[pallet::storage]
+	pub type Head<T: Config> =
+		StorageValue<_, UnstakeRequest<T::AccountId, MaxChecking<T>>, OptionQuery>;
+
+	/// The map of all accounts wishing to be unstaked.
+	///
+	/// Points the `AccountId` wishing to unstake to the optional `PoolId` they wish to join
+	/// thereafter.
+	#[pallet::storage]
+	pub type Queue<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, Option<PoolId>>;
+
+	/// Number of eras to check per block.
+	///
+	/// If set to 0, this pallet does absolutely nothing.
+	///
+	/// Based on the amount of weight available at `on_idle`, up to this many eras of a single
+	/// nominator might be checked.
+	#[pallet::storage]
+	pub type ErasToCheckPerBlock<T: Config> = StorageValue<_, u32, ValueQuery>;
+
+	/// The events of this pallet.
+	#[pallet::event]
+	#[pallet::generate_deposit(pub(super) fn deposit_event)]
+	pub enum Event<T: Config> {
+		/// A staker was unstaked.
+		Unstaked { stash: T::AccountId, maybe_pool_id: Option<PoolId>, result: DispatchResult },
+		/// A staker was slashed for requesting fast-unstake whilst being exposed.
+		Slashed { stash: T::AccountId, amount: BalanceOf<T> },
+		/// A staker was partially checked for the given eras, but the process did not finish.
+		Checking { stash: T::AccountId, eras: Vec<EraIndex> },
+		/// Some internal error happened while migrating stash. They are removed as head as a
+		/// consequence.
+		Errored { stash: T::AccountId },
+		/// An internal error happened. Operations will be paused now.
+		InternalError,
+	}
+
+	#[pallet::error]
+	#[cfg_attr(test, derive(PartialEq))]
+	pub enum Error<T> {
+		/// The provided Controller account was not found.
+		///
+		/// This means that the given account is not bonded.
+		NotController,
+		/// The bonded account has already been queued.
+		AlreadyQueued,
+		/// The bonded account has active unlocking chunks.
+		NotFullyBonded,
+		/// The provided un-staker is not in the `Queue`.
+		NotQueued,
+		/// The provided un-staker is already in Head, and cannot deregister.
+		AlreadyHead,
+	}
+
+	#[pallet::hooks]
+	impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {
+		fn on_idle(_: T::BlockNumber, remaining_weight: Weight) -> Weight {
+			if remaining_weight.any_lt(T::DbWeight::get().reads(2)) {
+				return Weight::from_ref_time(0)
+			}
+
+			Self::do_on_idle(remaining_weight)
+		}
+	}
+
+	#[pallet::call]
+	impl<T: Config> Pallet<T> {
+		/// Register oneself for fast-unstake.
+		///
+		/// The dispatch origin of this call must be signed by the controller account, similar to
+		/// `staking::unbond`.
+		///
+		/// The stash associated with the origin must have no ongoing unlocking chunks. If
+		/// successful, this will fully unbond and chill the stash. Then, it will enqueue the stash
+		/// to be checked in further blocks.
+		///
+		/// If by the time this is called, the stash is actually eligible for fast-unstake, then
+		/// they are guaranteed to remain eligible, because the call will chill them as well.
+		///
+		/// If the check works, the entire staking data is removed, i.e. the stash is fully
+		/// unstaked, and they potentially join a pool with their entire bonded stake.
+		///
+		/// If the check fails, the stash remains chilled and waiting for being unbonded as in with
+		/// the normal staking system, but they lose part of their unbonding chunks due to consuming
+		/// the chain's resources.
+		#[pallet::weight(<T as Config>::WeightInfo::register_fast_unstake())]
+		pub fn register_fast_unstake(
+			origin: OriginFor<T>,
+			maybe_pool_id: Option<PoolId>,
+		) -> DispatchResult {
+			let ctrl = ensure_signed(origin)?;
+
+			let ledger =
+				pallet_staking::Ledger::<T>::get(&ctrl).ok_or(Error::<T>::NotController)?;
+			ensure!(!Queue::<T>::contains_key(&ledger.stash), Error::<T>::AlreadyQueued);
+			ensure!(
+				Head::<T>::get().map_or(true, |UnstakeRequest { stash, .. }| stash != ledger.stash),
+				Error::<T>::AlreadyHead
+			);
+			// second part of the && is defensive.
+			ensure!(
+				ledger.active == ledger.total && ledger.unlocking.is_empty(),
+				Error::<T>::NotFullyBonded
+			);
+
+			// chill and fully unstake.
+			Staking::<T>::chill(RawOrigin::Signed(ctrl.clone()).into())?;
+			Staking::<T>::unbond(RawOrigin::Signed(ctrl).into(), ledger.total)?;
+
+			// enqueue them.
+			Queue::<T>::insert(ledger.stash, maybe_pool_id);
+			Ok(())
+		}
+
+		/// Deregister oneself from the fast-unstake (also cancels joining the pool if that was
+		/// supplied on `register_fast_unstake` .
+		///
+		/// This is useful if one is registered, they are still waiting, and they change their mind.
+		///
+		/// Note that the associated stash is still fully unbonded and chilled as a consequence of
+		/// calling `register_fast_unstake`. This should probably be followed by a call to
+		/// `Staking::rebond`.
+		#[pallet::weight(<T as Config>::WeightInfo::deregister())]
+		pub fn deregister(origin: OriginFor<T>) -> DispatchResult {
+			let ctrl = ensure_signed(origin)?;
+			let stash = pallet_staking::Ledger::<T>::get(&ctrl)
+				.map(|l| l.stash)
+				.ok_or(Error::<T>::NotController)?;
+			ensure!(Queue::<T>::contains_key(&stash), Error::<T>::NotQueued);
+			ensure!(
+				Head::<T>::get().map_or(true, |UnstakeRequest { stash, .. }| stash != stash),
+				Error::<T>::AlreadyHead
+			);
+			Queue::<T>::remove(stash);
+			Ok(())
+		}
+
+		/// Control the operation of this pallet.
+		///
+		/// Dispatch origin must be signed by the [`Config::ControlOrigin`].
+		#[pallet::weight(<T as Config>::WeightInfo::control())]
+		pub fn control(origin: OriginFor<T>, unchecked_eras_to_check: EraIndex) -> DispatchResult {
+			let _ = T::ControlOrigin::ensure_origin(origin)?;
+			ErasToCheckPerBlock::<T>::put(unchecked_eras_to_check);
+			Ok(())
+		}
+	}
+
+	impl<T: Config> Pallet<T> {
+		/// process up to `remaining_weight`.
+		///
+		/// Returns the actual weight consumed.
+		///
+		/// Written for readability in mind, not efficiency. For example:
+		///
+		/// 1. We assume this is only ever called once per `on_idle`. This is because we know that
+		/// in all use cases, even a single nominator cannot be unbonded in a single call. Multiple
+		/// calls to this function are thus not needed.
+		///
+		/// 2. We will only mark a staker as unstaked if at the beginning of a check cycle, they are
+		/// found out to have no eras to check. At the end of a check cycle, even if they are fully
+		/// checked, we don't finish the process.
+		pub(crate) fn do_on_idle(remaining_weight: Weight) -> Weight {
+			let mut eras_to_check_per_block = ErasToCheckPerBlock::<T>::get();
+			if eras_to_check_per_block.is_zero() {
+				return T::DbWeight::get().reads(1)
+			}
+
+			// NOTE: here we're assuming that the number of validators has only ever increased,
+			// meaning that the number of exposures to check is either this per era, or less.
+			let validator_count = pallet_staking::ValidatorCount::<T>::get();
+
+			// determine the number of eras to check. This is based on both `ErasToCheckPerBlock`
+			// and `remaining_weight` passed on to us from the runtime executive.
+			let max_weight = |v, u| {
+				<T as Config>::WeightInfo::on_idle_check(v * u)
+					.max(<T as Config>::WeightInfo::on_idle_unstake())
+			};
+			while max_weight(validator_count, eras_to_check_per_block).any_gt(remaining_weight) {
+				eras_to_check_per_block.saturating_dec();
+				if eras_to_check_per_block.is_zero() {
+					log!(debug, "early existing because eras_to_check_per_block is zero");
+					return T::DbWeight::get().reads(2)
+				}
+			}
+
+			if <T as pallet_staking::Config>::ElectionProvider::ongoing() {
+				// NOTE: we assume `ongoing` does not consume any weight.
+				// there is an ongoing election -- we better not do anything. Imagine someone is not
+				// exposed anywhere in the last era, and the snapshot for the election is already
+				// taken. In this time period, we don't want to accidentally unstake them.
+				return T::DbWeight::get().reads(2)
+			}
+
+			let UnstakeRequest { stash, mut checked, maybe_pool_id } = match Head::<T>::take()
+				.or_else(|| {
+					// NOTE: there is no order guarantees in `Queue`.
+					Queue::<T>::drain()
+						.map(|(stash, maybe_pool_id)| UnstakeRequest {
+							stash,
+							maybe_pool_id,
+							checked: Default::default(),
+						})
+						.next()
+				}) {
+				None => {
+					// There's no `Head` and nothing in the `Queue`, nothing to do here.
+					return T::DbWeight::get().reads(4)
+				},
+				Some(head) => head,
+			};
+
+			log!(
+				debug,
+				"checking {:?}, eras_to_check_per_block = {:?}, remaining_weight = {:?}",
+				stash,
+				eras_to_check_per_block,
+				remaining_weight
+			);
+
+			// the range that we're allowed to check in this round.
+			let current_era = pallet_staking::CurrentEra::<T>::get().unwrap_or_default();
+			let bonding_duration = <T as pallet_staking::Config>::BondingDuration::get();
+			// prune all the old eras that we don't care about. This will help us keep the bound
+			// of `checked`.
+			checked.retain(|e| *e >= current_era.saturating_sub(bonding_duration));
+			let unchecked_eras_to_check = {
+				// get the last available `bonding_duration` eras up to current era in reverse
+				// order.
+				let total_check_range = (current_era.saturating_sub(bonding_duration)..=
+					current_era)
+					.rev()
+					.collect::<Vec<_>>();
+				debug_assert!(
+					total_check_range.len() <= (bonding_duration + 1) as usize,
+					"{:?}",
+					total_check_range
+				);
+
+				// remove eras that have already been checked, take a maximum of
+				// eras_to_check_per_block.
+				total_check_range
+					.into_iter()
+					.filter(|e| !checked.contains(e))
+					.take(eras_to_check_per_block as usize)
+					.collect::<Vec<_>>()
+			};
+
+			log!(
+				debug,
+				"{} eras to check: {:?}",
+				unchecked_eras_to_check.len(),
+				unchecked_eras_to_check
+			);
+
+			if unchecked_eras_to_check.is_empty() {
+				// `stash` is not exposed in any era now -- we can let go of them now.
+				let num_slashing_spans = Staking::<T>::slashing_spans(&stash).iter().count() as u32;
+
+				let ctrl = match pallet_staking::Bonded::<T>::get(&stash) {
+					Some(ctrl) => ctrl,
+					None => {
+						Self::deposit_event(Event::<T>::Errored { stash });
+						return <T as Config>::WeightInfo::on_idle_unstake()
+					},
+				};
+
+				let ledger = match pallet_staking::Ledger::<T>::get(ctrl) {
+					Some(ledger) => ledger,
+					None => {
+						Self::deposit_event(Event::<T>::Errored { stash });
+						return <T as Config>::WeightInfo::on_idle_unstake()
+					},
+				};
+
+				let unstake_result = pallet_staking::Pallet::<T>::force_unstake(
+					RawOrigin::Root.into(),
+					stash.clone(),
+					num_slashing_spans,
+				);
+
+				let pool_stake_result = if let Some(pool_id) = maybe_pool_id {
+					pallet_nomination_pools::Pallet::<T>::join(
+						RawOrigin::Signed(stash.clone()).into(),
+						ledger.total,
+						pool_id,
+					)
+				} else {
+					Ok(())
+				};
+
+				let result = unstake_result.and(pool_stake_result);
+				log!(
+					info,
+					"unstaked {:?}, maybe_pool {:?}, outcome: {:?}",
+					stash,
+					maybe_pool_id,
+					result
+				);
+
+				Self::deposit_event(Event::<T>::Unstaked { stash, maybe_pool_id, result });
+				<T as Config>::WeightInfo::on_idle_unstake()
+			} else {
+				// eras remaining to be checked.
+				let mut eras_checked = 0u32;
+				let is_exposed = unchecked_eras_to_check.iter().any(|e| {
+					eras_checked.saturating_inc();
+					Self::is_exposed_in_era(&stash, e)
+				});
+
+				log!(
+					debug,
+					"checked {:?} eras, exposed? {}, (v: {:?}, u: {:?})",
+					eras_checked,
+					is_exposed,
+					validator_count,
+					unchecked_eras_to_check.len()
+				);
+
+				// NOTE: you can be extremely unlucky and get slashed here: You are not exposed in
+				// the last 28 eras, have registered yourself to be unstaked, midway being checked,
+				// you are exposed.
+				if is_exposed {
+					let amount = T::SlashPerEra::get()
+						.saturating_mul(eras_checked.saturating_add(checked.len() as u32).into());
+					pallet_staking::slashing::do_slash::<T>(
+						&stash,
+						amount,
+						&mut Default::default(),
+						&mut Default::default(),
+						current_era,
+					);
+					log!(info, "slashed {:?} by {:?}", stash, amount);
+					Self::deposit_event(Event::<T>::Slashed { stash, amount });
+				} else {
+					// Not exposed in these eras.
+					match checked.try_extend(unchecked_eras_to_check.clone().into_iter()) {
+						Ok(_) => {
+							Head::<T>::put(UnstakeRequest {
+								stash: stash.clone(),
+								checked,
+								maybe_pool_id,
+							});
+							Self::deposit_event(Event::<T>::Checking {
+								stash,
+								eras: unchecked_eras_to_check,
+							});
+						},
+						Err(_) => {
+							// don't put the head back in -- there is an internal error in the
+							// pallet.
+							frame_support::defensive!("`checked is pruned via retain above`");
+							ErasToCheckPerBlock::<T>::put(0);
+							Self::deposit_event(Event::<T>::InternalError);
+						},
+					}
+				}
+
+				<T as Config>::WeightInfo::on_idle_check(validator_count * eras_checked)
+			}
+		}
+
+		/// Checks whether an account `staker` has been exposed in an era.
+		fn is_exposed_in_era(staker: &T::AccountId, era: &EraIndex) -> bool {
+			pallet_staking::ErasStakers::<T>::iter_prefix(era).any(|(validator, exposures)| {
+				validator == *staker || exposures.others.iter().any(|i| i.who == *staker)
+			})
+		}
+	}
+}
diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs
new file mode 100644
index 00000000000..b9cf16e18e8
--- /dev/null
+++ b/substrate/frame/fast-unstake/src/mock.rs
@@ -0,0 +1,387 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::{self as fast_unstake};
+use frame_support::{
+	assert_ok,
+	pallet_prelude::*,
+	parameter_types,
+	traits::{ConstU64, ConstU8, Currency},
+	weights::constants::WEIGHT_PER_SECOND,
+	PalletId,
+};
+use sp_runtime::{
+	traits::{Convert, IdentityLookup},
+	FixedU128,
+};
+
+use frame_system::RawOrigin;
+use pallet_staking::{Exposure, IndividualExposure, StakerStatus};
+use sp_std::prelude::*;
+
+pub type AccountId = u128;
+pub type AccountIndex = u32;
+pub type BlockNumber = u64;
+pub type Balance = u128;
+pub type T = Runtime;
+
+parameter_types! {
+	pub BlockWeights: frame_system::limits::BlockWeights =
+		frame_system::limits::BlockWeights::simple_max(2u64 * WEIGHT_PER_SECOND);
+}
+
+impl frame_system::Config for Runtime {
+	type BaseCallFilter = frame_support::traits::Everything;
+	type BlockWeights = BlockWeights;
+	type BlockLength = ();
+	type DbWeight = ();
+	type RuntimeOrigin = RuntimeOrigin;
+	type Index = AccountIndex;
+	type BlockNumber = BlockNumber;
+	type RuntimeCall = RuntimeCall;
+	type Hash = sp_core::H256;
+	type Hashing = sp_runtime::traits::BlakeTwo256;
+	type AccountId = AccountId;
+	type Lookup = IdentityLookup<Self::AccountId>;
+	type Header = sp_runtime::testing::Header;
+	type RuntimeEvent = RuntimeEvent;
+	type BlockHashCount = ();
+	type Version = ();
+	type PalletInfo = PalletInfo;
+	type AccountData = pallet_balances::AccountData<Balance>;
+	type OnNewAccount = ();
+	type OnKilledAccount = ();
+	type SystemWeightInfo = ();
+	type SS58Prefix = ();
+	type OnSetCode = ();
+	type MaxConsumers = frame_support::traits::ConstU32<16>;
+}
+
+impl pallet_timestamp::Config for Runtime {
+	type Moment = u64;
+	type OnTimestampSet = ();
+	type MinimumPeriod = ConstU64<5>;
+	type WeightInfo = ();
+}
+
+parameter_types! {
+	pub static ExistentialDeposit: Balance = 1;
+}
+
+impl pallet_balances::Config for Runtime {
+	type MaxLocks = ConstU32<128>;
+	type MaxReserves = ();
+	type ReserveIdentifier = [u8; 8];
+	type Balance = Balance;
+	type RuntimeEvent = RuntimeEvent;
+	type DustRemoval = ();
+	type ExistentialDeposit = ExistentialDeposit;
+	type AccountStore = System;
+	type WeightInfo = ();
+}
+
+pallet_staking_reward_curve::build! {
+	const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!(
+		min_inflation: 0_025_000,
+		max_inflation: 0_100_000,
+		ideal_stake: 0_500_000,
+		falloff: 0_050_000,
+		max_piece_count: 40,
+		test_precision: 0_005_000,
+	);
+}
+
+parameter_types! {
+	pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
+	pub static BondingDuration: u32 = 3;
+	pub static CurrentEra: u32 = 0;
+	pub static Ongoing: bool = false;
+}
+
+pub struct MockElection;
+impl frame_election_provider_support::ElectionProvider for MockElection {
+	type AccountId = AccountId;
+	type BlockNumber = BlockNumber;
+	type DataProvider = Staking;
+	type Error = ();
+
+	fn ongoing() -> bool {
+		Ongoing::get()
+	}
+
+	fn elect() -> Result<frame_election_provider_support::Supports<AccountId>, Self::Error> {
+		Err(())
+	}
+}
+
+impl pallet_staking::Config for Runtime {
+	type MaxNominations = ConstU32<16>;
+	type Currency = Balances;
+	type CurrencyBalance = Balance;
+	type UnixTime = pallet_timestamp::Pallet<Self>;
+	type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
+	type RewardRemainder = ();
+	type RuntimeEvent = RuntimeEvent;
+	type Slash = ();
+	type Reward = ();
+	type SessionsPerEra = ();
+	type SlashDeferDuration = ();
+	type SlashCancelOrigin = frame_system::EnsureRoot<Self::AccountId>;
+	type BondingDuration = BondingDuration;
+	type SessionInterface = ();
+	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
+	type NextNewSession = ();
+	type HistoryDepth = ConstU32<84>;
+	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type OffendingValidatorsThreshold = ();
+	type ElectionProvider = MockElection;
+	type GenesisElectionProvider = Self::ElectionProvider;
+	type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
+	type TargetList = pallet_staking::UseValidatorsMap<Self>;
+	type MaxUnlockingChunks = ConstU32<32>;
+	type OnStakerSlash = Pools;
+	type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
+	type WeightInfo = ();
+}
+
+pub struct BalanceToU256;
+impl Convert<Balance, sp_core::U256> for BalanceToU256 {
+	fn convert(n: Balance) -> sp_core::U256 {
+		n.into()
+	}
+}
+
+pub struct U256ToBalance;
+impl Convert<sp_core::U256, Balance> for U256ToBalance {
+	fn convert(n: sp_core::U256) -> Balance {
+		n.try_into().unwrap()
+	}
+}
+
+parameter_types! {
+	pub const PostUnbondingPoolsWindow: u32 = 10;
+	pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls");
+	pub static MaxMetadataLen: u32 = 10;
+	pub static CheckLevel: u8 = 255;
+}
+
+impl pallet_nomination_pools::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type WeightInfo = ();
+	type Currency = Balances;
+	type CurrencyBalance = Balance;
+	type RewardCounter = FixedU128;
+	type BalanceToU256 = BalanceToU256;
+	type U256ToBalance = U256ToBalance;
+	type StakingInterface = Staking;
+	type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow;
+	type MaxMetadataLen = MaxMetadataLen;
+	type MaxUnbonding = ConstU32<8>;
+	type MaxPointsToBalance = ConstU8<10>;
+	type PalletId = PoolsPalletId;
+}
+
+parameter_types! {
+	pub static SlashPerEra: u32 = 100;
+}
+
+impl fast_unstake::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type SlashPerEra = SlashPerEra;
+	type ControlOrigin = frame_system::EnsureRoot<Self::AccountId>;
+	type WeightInfo = ();
+}
+
+type Block = frame_system::mocking::MockBlock<Runtime>;
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
+frame_support::construct_runtime!(
+	pub enum Runtime where
+		Block = Block,
+		NodeBlock = Block,
+		UncheckedExtrinsic = UncheckedExtrinsic
+	{
+		System: frame_system,
+		Timestamp: pallet_timestamp,
+		Balances: pallet_balances,
+		Staking: pallet_staking,
+		Pools: pallet_nomination_pools,
+		FastUnstake: fast_unstake,
+	}
+);
+
+parameter_types! {
+	static FastUnstakeEvents: u32 = 0;
+}
+
+pub(crate) fn fast_unstake_events_since_last_call() -> Vec<super::Event<Runtime>> {
+	let events = System::events()
+		.into_iter()
+		.map(|r| r.event)
+		.filter_map(|e| if let RuntimeEvent::FastUnstake(inner) = e { Some(inner) } else { None })
+		.collect::<Vec<_>>();
+	let already_seen = FastUnstakeEvents::get();
+	FastUnstakeEvents::set(events.len() as u32);
+	events.into_iter().skip(already_seen as usize).collect()
+}
+
+pub struct ExtBuilder {
+	exposed_nominators: Vec<(AccountId, AccountId, Balance)>,
+}
+
+impl Default for ExtBuilder {
+	fn default() -> Self {
+		Self {
+			exposed_nominators: vec![
+				(1, 2, 100),
+				(3, 4, 100),
+				(5, 6, 100),
+				(7, 8, 100),
+				(9, 10, 100),
+			],
+		}
+	}
+}
+
+pub(crate) const VALIDATORS_PER_ERA: AccountId = 32;
+pub(crate) const VALIDATOR_PREFIX: AccountId = 100;
+pub(crate) const NOMINATORS_PER_VALIDATOR_PER_ERA: AccountId = 4;
+pub(crate) const NOMINATOR_PREFIX: AccountId = 1000;
+
+impl ExtBuilder {
+	pub(crate) fn register_stakers_for_era(era: u32) {
+		// validators are prefixed with 100 and nominators with 1000 to prevent conflict. Make sure
+		// all the other accounts used in tests are below 100. Also ensure here that we don't
+		// overlap.
+		assert!(VALIDATOR_PREFIX + VALIDATORS_PER_ERA < NOMINATOR_PREFIX);
+
+		(VALIDATOR_PREFIX..VALIDATOR_PREFIX + VALIDATORS_PER_ERA)
+			.map(|v| {
+				// for the sake of sanity, let's register this taker as an actual validator.
+				let others = (NOMINATOR_PREFIX..
+					(NOMINATOR_PREFIX + NOMINATORS_PER_VALIDATOR_PER_ERA))
+					.map(|n| IndividualExposure { who: n, value: 0 as Balance })
+					.collect::<Vec<_>>();
+				(v, Exposure { total: 0, own: 0, others })
+			})
+			.for_each(|(validator, exposure)| {
+				pallet_staking::ErasStakers::<T>::insert(era, validator, exposure);
+			});
+	}
+
+	pub(crate) fn build(self) -> sp_io::TestExternalities {
+		sp_tracing::try_init_simple();
+		let mut storage =
+			frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
+
+		// create one default pool.
+		let _ = pallet_nomination_pools::GenesisConfig::<Runtime> { ..Default::default() }
+			.assimilate_storage(&mut storage);
+
+		let validators_range = VALIDATOR_PREFIX..VALIDATOR_PREFIX + VALIDATORS_PER_ERA;
+		let nominators_range =
+			NOMINATOR_PREFIX..NOMINATOR_PREFIX + NOMINATORS_PER_VALIDATOR_PER_ERA;
+
+		let _ = pallet_balances::GenesisConfig::<Runtime> {
+			balances: self
+				.exposed_nominators
+				.clone()
+				.into_iter()
+				.map(|(stash, _, balance)| (stash, balance * 2))
+				.chain(
+					self.exposed_nominators
+						.clone()
+						.into_iter()
+						.map(|(_, ctrl, balance)| (ctrl, balance * 2)),
+				)
+				.chain(validators_range.clone().map(|x| (x, 100)))
+				.chain(nominators_range.clone().map(|x| (x, 100)))
+				.collect::<Vec<_>>(),
+		}
+		.assimilate_storage(&mut storage);
+
+		let _ = pallet_staking::GenesisConfig::<Runtime> {
+			stakers: self
+				.exposed_nominators
+				.into_iter()
+				.map(|(x, y, z)| (x, y, z, pallet_staking::StakerStatus::Nominator(vec![42])))
+				.chain(validators_range.map(|x| (x, x, 100, StakerStatus::Validator)))
+				.chain(nominators_range.map(|x| (x, x, 100, StakerStatus::Nominator(vec![x]))))
+				.collect::<Vec<_>>(),
+			..Default::default()
+		}
+		.assimilate_storage(&mut storage);
+
+		let mut ext = sp_io::TestExternalities::from(storage);
+
+		ext.execute_with(|| {
+			// for events to be deposited.
+			frame_system::Pallet::<Runtime>::set_block_number(1);
+
+			for era in 0..=(BondingDuration::get()) {
+				Self::register_stakers_for_era(era);
+			}
+
+			// because we read this value as a measure of how many validators we have.
+			pallet_staking::ValidatorCount::<Runtime>::put(VALIDATORS_PER_ERA as u32);
+
+			// make a pool
+			let amount_to_bond = Pools::depositor_min_bond();
+			Balances::make_free_balance_be(&10, amount_to_bond * 5);
+			assert_ok!(Pools::create(RawOrigin::Signed(10).into(), amount_to_bond, 900, 901, 902));
+		});
+		ext
+	}
+
+	pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
+		self.build().execute_with(|| {
+			test();
+		})
+	}
+}
+
+pub(crate) fn run_to_block(n: u64, on_idle: bool) {
+	let current_block = System::block_number();
+	assert!(n > current_block);
+	while System::block_number() < n {
+		Balances::on_finalize(System::block_number());
+		Staking::on_finalize(System::block_number());
+		Pools::on_finalize(System::block_number());
+		FastUnstake::on_finalize(System::block_number());
+
+		System::set_block_number(System::block_number() + 1);
+
+		Balances::on_initialize(System::block_number());
+		Staking::on_initialize(System::block_number());
+		Pools::on_initialize(System::block_number());
+		FastUnstake::on_initialize(System::block_number());
+		if on_idle {
+			FastUnstake::on_idle(System::block_number(), BlockWeights::get().max_block);
+		}
+	}
+}
+
+pub(crate) fn next_block(on_idle: bool) {
+	let current = System::block_number();
+	run_to_block(current + 1, on_idle);
+}
+
+pub fn assert_unstaked(stash: &AccountId) {
+	assert!(!pallet_staking::Bonded::<T>::contains_key(stash));
+	assert!(!pallet_staking::Payee::<T>::contains_key(stash));
+	assert!(!pallet_staking::Validators::<T>::contains_key(stash));
+	assert!(!pallet_staking::Nominators::<T>::contains_key(stash));
+}
diff --git a/substrate/frame/fast-unstake/src/tests.rs b/substrate/frame/fast-unstake/src/tests.rs
new file mode 100644
index 00000000000..a51c1acdf06
--- /dev/null
+++ b/substrate/frame/fast-unstake/src/tests.rs
@@ -0,0 +1,1036 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests for pallet-fast-unstake.
+
+use super::*;
+use crate::{mock::*, types::*, weights::WeightInfo, Event};
+use frame_support::{assert_noop, assert_ok, bounded_vec, pallet_prelude::*, traits::Currency};
+use pallet_nomination_pools::{BondedPools, LastPoolId, RewardPools};
+use pallet_staking::{CurrentEra, IndividualExposure, RewardDestination};
+
+use sp_runtime::{traits::BadOrigin, DispatchError, ModuleError};
+use sp_staking::StakingInterface;
+
+#[test]
+fn test_setup_works() {
+	ExtBuilder::default().build_and_execute(|| {
+		assert_eq!(BondedPools::<T>::count(), 1);
+		assert_eq!(RewardPools::<T>::count(), 1);
+		assert_eq!(Staking::bonding_duration(), 3);
+		let last_pool = LastPoolId::<T>::get();
+		assert_eq!(last_pool, 1);
+	});
+}
+
+#[test]
+fn register_works() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Controller account registers for fast unstake.
+		assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+		// Ensure stash is in the queue.
+		assert_ne!(Queue::<T>::get(1), None);
+	});
+}
+
+#[test]
+fn cannot_register_if_not_bonded() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Mint accounts 1 and 2 with 200 tokens.
+		for _ in 1..2 {
+			let _ = Balances::make_free_balance_be(&1, 200);
+		}
+		// Attempt to fast unstake.
+		assert_noop!(
+			FastUnstake::register_fast_unstake(RuntimeOrigin::signed(1), Some(1_u32)),
+			Error::<T>::NotController
+		);
+	});
+}
+
+#[test]
+fn cannot_register_if_in_queue() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Insert some Queue item
+		Queue::<T>::insert(1, Some(1_u32));
+		// Cannot re-register, already in queue
+		assert_noop!(
+			FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)),
+			Error::<T>::AlreadyQueued
+		);
+	});
+}
+
+#[test]
+fn cannot_register_if_head() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Insert some Head item for stash
+		Head::<T>::put(UnstakeRequest { stash: 1, checked: bounded_vec![], maybe_pool_id: None });
+		// Controller attempts to regsiter
+		assert_noop!(
+			FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)),
+			Error::<T>::AlreadyHead
+		);
+	});
+}
+
+#[test]
+fn cannot_register_if_has_unlocking_chunks() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Start unbonding half of staked tokens
+		assert_ok!(Staking::unbond(RuntimeOrigin::signed(2), 50_u128));
+		// Cannot register for fast unstake with unlock chunks active
+		assert_noop!(
+			FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)),
+			Error::<T>::NotFullyBonded
+		);
+	});
+}
+
+#[test]
+fn deregister_works() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Controller account registers for fast unstake.
+		assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+		// Controller then changes mind and deregisters.
+		assert_ok!(FastUnstake::deregister(RuntimeOrigin::signed(2)));
+		// Ensure stash no longer exists in the queue.
+		assert_eq!(Queue::<T>::get(1), None);
+	});
+}
+
+#[test]
+fn cannot_deregister_if_not_controller() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Controller account registers for fast unstake.
+		assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+		// Stash tries to deregister.
+		assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(1)), Error::<T>::NotController);
+	});
+}
+
+#[test]
+fn cannot_deregister_if_not_queued() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Controller tries to deregister without first registering
+		assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::<T>::NotQueued);
+	});
+}
+
+#[test]
+fn cannot_deregister_already_head() {
+	ExtBuilder::default().build_and_execute(|| {
+		// Controller attempts to register, should fail
+		assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+		// Insert some Head item for stash.
+		Head::<T>::put(UnstakeRequest { stash: 1, checked: bounded_vec![], maybe_pool_id: None });
+		// Controller attempts to deregister
+		assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::<T>::AlreadyHead);
+	});
+}
+
+#[test]
+fn control_works() {
+	ExtBuilder::default().build_and_execute(|| {
+		// account with control (root) origin wants to only check 1 era per block.
+		assert_ok!(FastUnstake::control(RuntimeOrigin::root(), 1_u32));
+	});
+}
+
+#[test]
+fn control_must_be_control_origin() {
+	ExtBuilder::default().build_and_execute(|| {
+		// account without control (root) origin wants to only check 1 era per block.
+		assert_noop!(FastUnstake::control(RuntimeOrigin::signed(1), 1_u32), BadOrigin);
+	});
+}
+
+mod on_idle {
+	use super::*;
+
+	#[test]
+	fn early_exit() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// set up Queue item
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1)));
+			assert_eq!(Queue::<T>::get(1), Some(Some(1)));
+
+			// call on_idle with no remaining weight
+			FastUnstake::on_idle(System::block_number(), Weight::from_ref_time(0));
+
+			// assert nothing changed in Queue and Head
+			assert_eq!(Head::<T>::get(), None);
+			assert_eq!(Queue::<T>::get(1), Some(Some(1)));
+		});
+	}
+
+	#[test]
+	fn respects_weight() {
+		ExtBuilder::default().build_and_execute(|| {
+			// we want to check all eras in one block...
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// given
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1)));
+			assert_eq!(Queue::<T>::get(1), Some(Some(1)));
+
+			assert_eq!(Queue::<T>::count(), 1);
+			assert_eq!(Head::<T>::get(), None);
+
+			// when: call fast unstake with not enough weight to process the whole thing, just one
+			// era.
+			let remaining_weight = <T as Config>::WeightInfo::on_idle_check(
+				pallet_staking::ValidatorCount::<T>::get() * 1,
+			);
+			assert_eq!(FastUnstake::on_idle(0, remaining_weight), remaining_weight);
+
+			// then
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![Event::Checking { stash: 1, eras: vec![3] }]
+			);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: Some(1) })
+			);
+
+			// when: another 1 era.
+			let remaining_weight = <T as Config>::WeightInfo::on_idle_check(
+				pallet_staking::ValidatorCount::<T>::get() * 1,
+			);
+			assert_eq!(FastUnstake::on_idle(0, remaining_weight), remaining_weight);
+
+			// then:
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![Event::Checking { stash: 1, eras: bounded_vec![2] }]
+			);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// when: then 5 eras, we only need 2 more.
+			let remaining_weight = <T as Config>::WeightInfo::on_idle_check(
+				pallet_staking::ValidatorCount::<T>::get() * 5,
+			);
+			assert_eq!(
+				FastUnstake::on_idle(0, remaining_weight),
+				// note the amount of weight consumed: 2 eras worth of weight.
+				<T as Config>::WeightInfo::on_idle_check(
+					pallet_staking::ValidatorCount::<T>::get() * 2,
+				)
+			);
+
+			// then:
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![Event::Checking { stash: 1, eras: vec![1, 0] }]
+			);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// when: not enough weight to unstake:
+			let remaining_weight =
+				<T as Config>::WeightInfo::on_idle_unstake() - Weight::from_ref_time(1);
+			assert_eq!(FastUnstake::on_idle(0, remaining_weight), Weight::from_ref_time(0));
+
+			// then nothing happens:
+			assert_eq!(fast_unstake_events_since_last_call(), vec![]);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// when: enough weight to get over at least one iteration: then we are unblocked and can
+			// unstake.
+			let remaining_weight = <T as Config>::WeightInfo::on_idle_check(
+				pallet_staking::ValidatorCount::<T>::get() * 1,
+			);
+			assert_eq!(
+				FastUnstake::on_idle(0, remaining_weight),
+				<T as Config>::WeightInfo::on_idle_unstake()
+			);
+
+			// then we finish the unbonding:
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) }]
+			);
+			assert_eq!(Head::<T>::get(), None,);
+
+			assert_unstaked(&1);
+		});
+	}
+
+	#[test]
+	fn if_head_not_set_one_random_fetched_from_queue() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// given
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None));
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4), None));
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(6), None));
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(8), None));
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(10), None));
+
+			assert_eq!(Queue::<T>::count(), 5);
+			assert_eq!(Head::<T>::get(), None);
+
+			// when
+			next_block(true);
+
+			// then
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: None
+				})
+			);
+			assert_eq!(Queue::<T>::count(), 4);
+
+			// when
+			next_block(true);
+
+			// then
+			assert_eq!(Head::<T>::get(), None,);
+			assert_eq!(Queue::<T>::count(), 4);
+
+			// when
+			next_block(true);
+
+			// then
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 5,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: None
+				}),
+			);
+			assert_eq!(Queue::<T>::count(), 3);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked { stash: 1, maybe_pool_id: None, result: Ok(()) },
+					Event::Checking { stash: 5, eras: vec![3, 2, 1, 0] }
+				]
+			);
+		});
+	}
+
+	#[test]
+	fn successful_multi_queue() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register multi accounts for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1)));
+			assert_eq!(Queue::<T>::get(1), Some(Some(1)));
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4), Some(1)));
+			assert_eq!(Queue::<T>::get(3), Some(Some(1)));
+
+			// assert 2 queue items are in Queue & None in Head to start with
+			assert_eq!(Queue::<T>::count(), 2);
+			assert_eq!(Head::<T>::get(), None);
+
+			// process on idle and check eras for next Queue item
+			next_block(true);
+
+			// process on idle & let go of current Head
+			next_block(true);
+
+			// confirm Head / Queue items remaining
+			assert_eq!(Queue::<T>::count(), 1);
+			assert_eq!(Head::<T>::get(), None);
+
+			// process on idle and check eras for next Queue item
+			next_block(true);
+
+			// process on idle & let go of current Head
+			next_block(true);
+
+			// Head & Queue should now be empty
+			assert_eq!(Head::<T>::get(), None);
+			assert_eq!(Queue::<T>::count(), 0);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) },
+					Event::Checking { stash: 3, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked { stash: 3, maybe_pool_id: Some(1), result: Ok(()) },
+				]
+			);
+
+			assert_unstaked(&1);
+			assert_unstaked(&3);
+		});
+	}
+
+	#[test]
+	fn successful_unstake_without_pool_join() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None));
+			assert_eq!(Queue::<T>::get(1), Some(None));
+
+			// process on idle
+			next_block(true);
+
+			// assert queue item has been moved to head
+			assert_eq!(Queue::<T>::get(1), None);
+
+			// assert head item present
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: None
+				})
+			);
+
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None,);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked { stash: 1, maybe_pool_id: None, result: Ok(()) }
+				]
+			);
+			assert_unstaked(&1);
+		});
+	}
+
+	#[test]
+	fn successful_unstake_joining_bad_pool() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(0)));
+			assert_eq!(Queue::<T>::get(1), Some(Some(0)));
+
+			// process on idle
+			next_block(true);
+
+			// assert queue item has been moved to head
+			assert_eq!(Queue::<T>::get(1), None);
+
+			// assert head item present
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: Some(0)
+				})
+			);
+
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None,);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked {
+						stash: 1,
+						maybe_pool_id: Some(0),
+						result: Err(DispatchError::Module(ModuleError {
+							index: 4,
+							error: [0, 0, 0, 0],
+							message: None
+						}))
+					}
+				]
+			);
+			assert_unstaked(&1);
+		});
+	}
+
+	#[test]
+	fn successful_unstake_all_eras_per_block() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+			assert_eq!(Queue::<T>::get(1), Some(Some(1)));
+
+			// process on idle
+			next_block(true);
+
+			// assert queue item has been moved to head
+			assert_eq!(Queue::<T>::get(1), None);
+
+			// assert head item present
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None,);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) }
+				]
+			);
+			assert_unstaked(&1);
+			assert!(pallet_nomination_pools::PoolMembers::<T>::contains_key(&1));
+		});
+	}
+
+	#[test]
+	fn successful_unstake_one_era_per_block() {
+		ExtBuilder::default().build_and_execute(|| {
+			// put 1 era per block
+			ErasToCheckPerBlock::<T>::put(1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+			assert_eq!(Queue::<T>::get(1), Some(Some(1)));
+
+			// process on idle
+			next_block(true);
+
+			// assert queue item has been moved to head
+			assert_eq!(Queue::<T>::get(1), None);
+
+			// assert head item present
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: Some(1) })
+			);
+
+			next_block(true);
+
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			next_block(true);
+
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			next_block(true);
+
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			next_block(true);
+
+			assert_eq!(Head::<T>::get(), None,);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3] },
+					Event::Checking { stash: 1, eras: vec![2] },
+					Event::Checking { stash: 1, eras: vec![1] },
+					Event::Checking { stash: 1, eras: vec![0] },
+					Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) }
+				]
+			);
+			assert_unstaked(&1);
+			assert!(pallet_nomination_pools::PoolMembers::<T>::contains_key(&1));
+		});
+	}
+
+	#[test]
+	fn old_checked_era_pruned() {
+		// the only scenario where checked era pruning (checked.retain) comes handy is a follows:
+		// the whole vector is full and at capacity and in the next call we are ready to unstake,
+		// but then a new era happens.
+		ExtBuilder::default().build_and_execute(|| {
+			// given
+			ErasToCheckPerBlock::<T>::put(1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None));
+			assert_eq!(Queue::<T>::get(1), Some(None));
+
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: None })
+			);
+
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2], maybe_pool_id: None })
+			);
+
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1],
+					maybe_pool_id: None
+				})
+			);
+
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: None
+				})
+			);
+
+			// when: a new era happens right before one is free.
+			CurrentEra::<T>::put(CurrentEra::<T>::get().unwrap() + 1);
+			ExtBuilder::register_stakers_for_era(CurrentEra::<T>::get().unwrap());
+
+			// then
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					// note era 0 is pruned to keep the vector length sane.
+					checked: bounded_vec![3, 2, 1, 4],
+					maybe_pool_id: None
+				})
+			);
+
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3] },
+					Event::Checking { stash: 1, eras: vec![2] },
+					Event::Checking { stash: 1, eras: vec![1] },
+					Event::Checking { stash: 1, eras: vec![0] },
+					Event::Checking { stash: 1, eras: vec![4] },
+					Event::Unstaked { stash: 1, maybe_pool_id: None, result: Ok(()) }
+				]
+			);
+			assert_unstaked(&1);
+		});
+	}
+
+	#[test]
+	fn unstake_paused_mid_election() {
+		ExtBuilder::default().build_and_execute(|| {
+			// give: put 1 era per block
+			ErasToCheckPerBlock::<T>::put(1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// register for fast unstake
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), Some(1_u32)));
+
+			// process 2 blocks
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: Some(1) })
+			);
+
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// when
+			Ongoing::set(true);
+
+			// then nothing changes
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// then we register a new era.
+			Ongoing::set(false);
+			CurrentEra::<T>::put(CurrentEra::<T>::get().unwrap() + 1);
+			ExtBuilder::register_stakers_for_era(CurrentEra::<T>::get().unwrap());
+
+			// then we can progress again, but notice that the new era that had to be checked.
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 4],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// progress to end
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 1,
+					checked: bounded_vec![3, 2, 4, 1],
+					maybe_pool_id: Some(1)
+				})
+			);
+
+			// but notice that we don't care about era 0 instead anymore! we're done.
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 1, eras: vec![3] },
+					Event::Checking { stash: 1, eras: vec![2] },
+					Event::Checking { stash: 1, eras: vec![4] },
+					Event::Checking { stash: 1, eras: vec![1] },
+					Event::Unstaked { stash: 1, maybe_pool_id: Some(1), result: Ok(()) }
+				]
+			);
+
+			assert_unstaked(&1);
+			assert!(pallet_nomination_pools::PoolMembers::<T>::contains_key(&1));
+		});
+	}
+
+	#[test]
+	fn exposed_nominator_cannot_unstake() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(1);
+			SlashPerEra::set(7);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// create an exposed nominator in era 1
+			let exposed = 666 as AccountId;
+			pallet_staking::ErasStakers::<T>::mutate(1, VALIDATORS_PER_ERA, |expo| {
+				expo.others.push(IndividualExposure { who: exposed, value: 0 as Balance });
+			});
+			Balances::make_free_balance_be(&exposed, 100);
+			assert_ok!(Staking::bond(
+				RuntimeOrigin::signed(exposed),
+				exposed,
+				10,
+				RewardDestination::Staked
+			));
+			assert_ok!(Staking::nominate(RuntimeOrigin::signed(exposed), vec![exposed]));
+
+			// register the exposed one.
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(exposed), None));
+
+			// a few blocks later, we realize they are slashed
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: exposed,
+					checked: bounded_vec![3],
+					maybe_pool_id: None
+				})
+			);
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: exposed,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: None
+				})
+			);
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				// we slash them by 21, since we checked 3 eras in total (3, 2, 1).
+				vec![
+					Event::Checking { stash: exposed, eras: vec![3] },
+					Event::Checking { stash: exposed, eras: vec![2] },
+					Event::Slashed { stash: exposed, amount: 3 * 7 }
+				]
+			);
+		});
+	}
+
+	#[test]
+	fn exposed_nominator_cannot_unstake_multi_check() {
+		ExtBuilder::default().build_and_execute(|| {
+			// same as the previous check, but we check 2 eras per block, and we make the exposed be
+			// exposed in era 0, so that it is detected halfway in a check era.
+			ErasToCheckPerBlock::<T>::put(2);
+			SlashPerEra::set(7);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// create an exposed nominator in era 1
+			let exposed = 666 as AccountId;
+			pallet_staking::ErasStakers::<T>::mutate(0, VALIDATORS_PER_ERA, |expo| {
+				expo.others.push(IndividualExposure { who: exposed, value: 0 as Balance });
+			});
+			Balances::make_free_balance_be(&exposed, 100);
+			assert_ok!(Staking::bond(
+				RuntimeOrigin::signed(exposed),
+				exposed,
+				10,
+				RewardDestination::Staked
+			));
+			assert_ok!(Staking::nominate(RuntimeOrigin::signed(exposed), vec![exposed]));
+
+			// register the exposed one.
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(exposed), None));
+
+			// a few blocks later, we realize they are slashed
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: exposed,
+					checked: bounded_vec![3, 2],
+					maybe_pool_id: None
+				})
+			);
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				// we slash them by 28, since we checked 4 eras in total.
+				vec![
+					Event::Checking { stash: exposed, eras: vec![3, 2] },
+					Event::Slashed { stash: exposed, amount: 4 * 7 }
+				]
+			);
+		});
+	}
+
+	#[test]
+	fn validators_cannot_bail() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// a validator switches role and register...
+			assert_ok!(Staking::nominate(
+				RuntimeOrigin::signed(VALIDATOR_PREFIX),
+				vec![VALIDATOR_PREFIX]
+			));
+			assert_ok!(FastUnstake::register_fast_unstake(
+				RuntimeOrigin::signed(VALIDATOR_PREFIX),
+				None
+			));
+
+			// but they indeed are exposed!
+			assert!(pallet_staking::ErasStakers::<T>::contains_key(
+				BondingDuration::get() - 1,
+				VALIDATOR_PREFIX
+			));
+
+			// process a block, this validator is exposed and has been slashed.
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![Event::Slashed { stash: 100, amount: 100 }]
+			);
+		});
+	}
+
+	#[test]
+	fn unexposed_validator_can_fast_unstake() {
+		ExtBuilder::default().build_and_execute(|| {
+			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
+			CurrentEra::<T>::put(BondingDuration::get());
+
+			// create a new validator that 100% not exposed.
+			Balances::make_free_balance_be(&42, 100);
+			assert_ok!(Staking::bond(RuntimeOrigin::signed(42), 42, 10, RewardDestination::Staked));
+			assert_ok!(Staking::validate(RuntimeOrigin::signed(42), Default::default()));
+
+			// let them register:
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(42), None));
+
+			// 2 block's enough to unstake them.
+			next_block(true);
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest {
+					stash: 42,
+					checked: bounded_vec![3, 2, 1, 0],
+					maybe_pool_id: None
+				})
+			);
+			next_block(true);
+			assert_eq!(Head::<T>::get(), None);
+
+			assert_eq!(
+				fast_unstake_events_since_last_call(),
+				vec![
+					Event::Checking { stash: 42, eras: vec![3, 2, 1, 0] },
+					Event::Unstaked { stash: 42, maybe_pool_id: None, result: Ok(()) }
+				]
+			);
+		});
+	}
+}
+
+mod signed_extension {
+	use super::*;
+	use sp_runtime::traits::SignedExtension;
+
+	const STAKING_CALL: crate::mock::RuntimeCall =
+		crate::mock::RuntimeCall::Staking(pallet_staking::Call::<T>::chill {});
+
+	#[test]
+	fn does_nothing_if_not_queued() {
+		ExtBuilder::default().build_and_execute(|| {
+			assert!(PreventStakingOpsIfUnbonding::<T>::new()
+				.pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default())
+				.is_ok());
+		})
+	}
+
+	#[test]
+	fn prevents_queued() {
+		ExtBuilder::default().build_and_execute(|| {
+			// given: stash for 2 is 1.
+			// when
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None));
+
+			// then
+			// stash can't.
+			assert!(PreventStakingOpsIfUnbonding::<T>::new()
+				.pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default())
+				.is_err());
+
+			// controller can't.
+			assert!(PreventStakingOpsIfUnbonding::<T>::new()
+				.pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default())
+				.is_err());
+		})
+	}
+
+	#[test]
+	fn prevents_head_stash() {
+		ExtBuilder::default().build_and_execute(|| {
+			// given: stash for 2 is 1.
+			// when
+			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2), None));
+
+			ErasToCheckPerBlock::<T>::put(1);
+			CurrentEra::<T>::put(BondingDuration::get());
+			next_block(true);
+
+			assert_eq!(
+				Head::<T>::get(),
+				Some(UnstakeRequest { stash: 1, checked: bounded_vec![3], maybe_pool_id: None })
+			);
+
+			// then
+			// stash can't
+			assert!(PreventStakingOpsIfUnbonding::<T>::new()
+				.pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default())
+				.is_err());
+
+			// controller can't
+			assert!(PreventStakingOpsIfUnbonding::<T>::new()
+				.pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default())
+				.is_err());
+		})
+	}
+}
diff --git a/substrate/frame/fast-unstake/src/types.rs b/substrate/frame/fast-unstake/src/types.rs
new file mode 100644
index 00000000000..ae8702e56a8
--- /dev/null
+++ b/substrate/frame/fast-unstake/src/types.rs
@@ -0,0 +1,119 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Types used in the Fast Unstake pallet.
+
+use crate::*;
+use codec::{Decode, Encode, MaxEncodedLen};
+use frame_support::{
+	traits::{Currency, Get, IsSubType},
+	BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
+};
+use pallet_nomination_pools::PoolId;
+use scale_info::TypeInfo;
+use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError};
+use sp_staking::EraIndex;
+use sp_std::{fmt::Debug, prelude::*};
+
+pub type BalanceOf<T> = <<T as pallet_staking::Config>::Currency as Currency<
+	<T as frame_system::Config>::AccountId,
+>>::Balance;
+
+/// An unstake request.
+#[derive(
+	Encode, Decode, EqNoBound, PartialEqNoBound, Clone, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen,
+)]
+pub struct UnstakeRequest<AccountId: Eq + PartialEq + Debug, MaxChecked: Get<u32>> {
+	/// Their stash account.
+	pub(crate) stash: AccountId,
+	/// The list of eras for which they have been checked.
+	pub(crate) checked: BoundedVec<EraIndex, MaxChecked>,
+	/// The pool they wish to join, if any.
+	pub(crate) maybe_pool_id: Option<PoolId>,
+}
+
+#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound)]
+#[scale_info(skip_type_params(T))]
+pub struct PreventStakingOpsIfUnbonding<T: Config + Send + Sync>(sp_std::marker::PhantomData<T>);
+
+#[cfg(test)]
+impl<T: Config + Send + Sync> PreventStakingOpsIfUnbonding<T> {
+	pub fn new() -> Self {
+		Self(Default::default())
+	}
+}
+
+impl<T: Config + Send + Sync> sp_runtime::traits::SignedExtension
+	for PreventStakingOpsIfUnbonding<T>
+where
+	<T as frame_system::Config>::RuntimeCall: IsSubType<pallet_staking::Call<T>>,
+{
+	type AccountId = T::AccountId;
+	type Call = <T as frame_system::Config>::RuntimeCall;
+	type AdditionalSigned = ();
+	type Pre = ();
+	const IDENTIFIER: &'static str = "PreventStakingOpsIfUnbonding";
+
+	fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> {
+		Ok(())
+	}
+
+	fn pre_dispatch(
+		self,
+		// NOTE: we want to prevent this stash-controller pair from doing anything in the
+		// staking system as long as they are registered here.
+		stash_or_controller: &Self::AccountId,
+		call: &Self::Call,
+		_info: &sp_runtime::traits::DispatchInfoOf<Self::Call>,
+		_len: usize,
+	) -> Result<Self::Pre, TransactionValidityError> {
+		// we don't check this in the tx-pool as it requires a storage read.
+		if <Self::Call as IsSubType<pallet_staking::Call<T>>>::is_sub_type(call).is_some() {
+			let check_stash = |stash: &T::AccountId| {
+				if Queue::<T>::contains_key(&stash) ||
+					Head::<T>::get().map_or(false, |u| &u.stash == stash)
+				{
+					Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
+				} else {
+					Ok(())
+				}
+			};
+			match (
+				// mapped from controller.
+				pallet_staking::Ledger::<T>::get(&stash_or_controller),
+				// mapped from stash.
+				pallet_staking::Bonded::<T>::get(&stash_or_controller),
+			) {
+				(Some(ledger), None) => {
+					// it is a controller.
+					check_stash(&ledger.stash)
+				},
+				(_, Some(_)) => {
+					// it's a stash.
+					let stash = stash_or_controller;
+					check_stash(stash)
+				},
+				(None, None) => {
+					// They are not a staker -- let them execute.
+					Ok(())
+				},
+			}
+		} else {
+			Ok(())
+		}
+	}
+}
diff --git a/substrate/frame/fast-unstake/src/weights.rs b/substrate/frame/fast-unstake/src/weights.rs
new file mode 100644
index 00000000000..04857d0dcc8
--- /dev/null
+++ b/substrate/frame/fast-unstake/src/weights.rs
@@ -0,0 +1,210 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Autogenerated weights for pallet_fast_unstake
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2022-09-07, STEPS: `10`, REPEAT: 1, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! HOSTNAME: `Kians-MacBook-Pro-2.local`, CPU: `<UNKNOWN>`
+//! EXECUTION: Some(Native), WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
+
+// Executed Command:
+// target/release/substrate
+// benchmark
+// pallet
+// --steps=10
+// --repeat=1
+// --pallet=pallet_fast_unstake
+// --extrinsic=*
+// --execution=native
+// --output
+// weight.rs
+// --template
+// ./.maintain/frame-weight-template.hbs
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for pallet_fast_unstake.
+pub trait WeightInfo {
+	fn on_idle_unstake() -> Weight;
+	fn on_idle_check(x: u32, ) -> Weight;
+	fn register_fast_unstake() -> Weight;
+	fn deregister() -> Weight;
+	fn control() -> Weight;
+}
+
+/// Weights for pallet_fast_unstake using the Substrate node and recommended hardware.
+pub struct SubstrateWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
+	// Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0)
+	// Storage: Staking ValidatorCount (r:1 w:0)
+	// Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0)
+	// Storage: FastUnstake Head (r:1 w:1)
+	// Storage: Staking CurrentEra (r:1 w:0)
+	// Storage: Staking SlashingSpans (r:1 w:0)
+	// Storage: Staking Bonded (r:2 w:1)
+	// Storage: Staking Ledger (r:2 w:2)
+	// Storage: Staking Validators (r:1 w:0)
+	// Storage: Staking Nominators (r:1 w:0)
+	// Storage: System Account (r:3 w:2)
+	// Storage: Balances Locks (r:2 w:2)
+	// Storage: NominationPools MinJoinBond (r:1 w:0)
+	// Storage: NominationPools PoolMembers (r:1 w:1)
+	// Storage: NominationPools BondedPools (r:1 w:1)
+	// Storage: NominationPools RewardPools (r:1 w:1)
+	// Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0)
+	// Storage: NominationPools MaxPoolMembers (r:1 w:0)
+	// Storage: NominationPools CounterForPoolMembers (r:1 w:1)
+	// Storage: BagsList ListNodes (r:1 w:0)
+	// Storage: Staking Payee (r:0 w:1)
+	fn on_idle_unstake() -> Weight {
+		Weight::from_ref_time(102_000_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(25 as u64))
+			.saturating_add(T::DbWeight::get().writes(13 as u64))
+	}
+	// Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0)
+	// Storage: Staking ValidatorCount (r:1 w:0)
+	// Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0)
+	// Storage: FastUnstake Head (r:1 w:1)
+	// Storage: FastUnstake Queue (r:2 w:1)
+	// Storage: FastUnstake CounterForQueue (r:1 w:1)
+	// Storage: Staking CurrentEra (r:1 w:0)
+	// Storage: Staking ErasStakers (r:1344 w:0)
+	/// The range of component `x` is `[672, 86016]`.
+	fn on_idle_check(x: u32, ) -> Weight {
+		Weight::from_ref_time(0 as u64)
+			// Standard Error: 244_000
+			.saturating_add(Weight::from_ref_time(13_913_000 as u64).saturating_mul(x as u64))
+			.saturating_add(T::DbWeight::get().reads(585 as u64))
+			.saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(x as u64)))
+			.saturating_add(T::DbWeight::get().writes(3 as u64))
+	}
+	// Storage: Staking Ledger (r:1 w:1)
+	// Storage: Staking Nominators (r:1 w:1)
+	// Storage: FastUnstake Queue (r:1 w:1)
+	// Storage: FastUnstake Head (r:1 w:0)
+	// Storage: Staking Validators (r:1 w:0)
+	// Storage: Staking CounterForNominators (r:1 w:1)
+	// Storage: BagsList ListNodes (r:1 w:1)
+	// Storage: BagsList ListBags (r:1 w:1)
+	// Storage: BagsList CounterForListNodes (r:1 w:1)
+	// Storage: Staking CurrentEra (r:1 w:0)
+	// Storage: Balances Locks (r:1 w:1)
+	// Storage: FastUnstake CounterForQueue (r:1 w:1)
+	fn register_fast_unstake() -> Weight {
+		Weight::from_ref_time(57_000_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(12 as u64))
+			.saturating_add(T::DbWeight::get().writes(9 as u64))
+	}
+	// Storage: Staking Ledger (r:1 w:0)
+	// Storage: FastUnstake Queue (r:1 w:1)
+	// Storage: FastUnstake Head (r:1 w:0)
+	// Storage: FastUnstake CounterForQueue (r:1 w:1)
+	fn deregister() -> Weight {
+		Weight::from_ref_time(15_000_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(4 as u64))
+			.saturating_add(T::DbWeight::get().writes(2 as u64))
+	}
+	// Storage: FastUnstake ErasToCheckPerBlock (r:0 w:1)
+	fn control() -> Weight {
+		Weight::from_ref_time(3_000_000 as u64)
+			.saturating_add(T::DbWeight::get().writes(1 as u64))
+	}
+}
+
+// For backwards compatibility and tests
+impl WeightInfo for () {
+	// Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0)
+	// Storage: Staking ValidatorCount (r:1 w:0)
+	// Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0)
+	// Storage: FastUnstake Head (r:1 w:1)
+	// Storage: Staking CurrentEra (r:1 w:0)
+	// Storage: Staking SlashingSpans (r:1 w:0)
+	// Storage: Staking Bonded (r:2 w:1)
+	// Storage: Staking Ledger (r:2 w:2)
+	// Storage: Staking Validators (r:1 w:0)
+	// Storage: Staking Nominators (r:1 w:0)
+	// Storage: System Account (r:3 w:2)
+	// Storage: Balances Locks (r:2 w:2)
+	// Storage: NominationPools MinJoinBond (r:1 w:0)
+	// Storage: NominationPools PoolMembers (r:1 w:1)
+	// Storage: NominationPools BondedPools (r:1 w:1)
+	// Storage: NominationPools RewardPools (r:1 w:1)
+	// Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0)
+	// Storage: NominationPools MaxPoolMembers (r:1 w:0)
+	// Storage: NominationPools CounterForPoolMembers (r:1 w:1)
+	// Storage: BagsList ListNodes (r:1 w:0)
+	// Storage: Staking Payee (r:0 w:1)
+	fn on_idle_unstake() -> Weight {
+		Weight::from_ref_time(102_000_000 as u64)
+			.saturating_add(RocksDbWeight::get().reads(25 as u64))
+			.saturating_add(RocksDbWeight::get().writes(13 as u64))
+	}
+	// Storage: FastUnstake ErasToCheckPerBlock (r:1 w:0)
+	// Storage: Staking ValidatorCount (r:1 w:0)
+	// Storage: ElectionProviderMultiPhase CurrentPhase (r:1 w:0)
+	// Storage: FastUnstake Head (r:1 w:1)
+	// Storage: FastUnstake Queue (r:2 w:1)
+	// Storage: FastUnstake CounterForQueue (r:1 w:1)
+	// Storage: Staking CurrentEra (r:1 w:0)
+	// Storage: Staking ErasStakers (r:1344 w:0)
+	/// The range of component `x` is `[672, 86016]`.
+	fn on_idle_check(x: u32, ) -> Weight {
+		Weight::from_ref_time(0 as u64)
+			// Standard Error: 244_000
+			.saturating_add(Weight::from_ref_time(13_913_000 as u64).saturating_mul(x as u64))
+			.saturating_add(RocksDbWeight::get().reads(585 as u64))
+			.saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(x as u64)))
+			.saturating_add(RocksDbWeight::get().writes(3 as u64))
+	}
+	// Storage: Staking Ledger (r:1 w:1)
+	// Storage: Staking Nominators (r:1 w:1)
+	// Storage: FastUnstake Queue (r:1 w:1)
+	// Storage: FastUnstake Head (r:1 w:0)
+	// Storage: Staking Validators (r:1 w:0)
+	// Storage: Staking CounterForNominators (r:1 w:1)
+	// Storage: BagsList ListNodes (r:1 w:1)
+	// Storage: BagsList ListBags (r:1 w:1)
+	// Storage: BagsList CounterForListNodes (r:1 w:1)
+	// Storage: Staking CurrentEra (r:1 w:0)
+	// Storage: Balances Locks (r:1 w:1)
+	// Storage: FastUnstake CounterForQueue (r:1 w:1)
+	fn register_fast_unstake() -> Weight {
+		Weight::from_ref_time(57_000_000 as u64)
+			.saturating_add(RocksDbWeight::get().reads(12 as u64))
+			.saturating_add(RocksDbWeight::get().writes(9 as u64))
+	}
+	// Storage: Staking Ledger (r:1 w:0)
+	// Storage: FastUnstake Queue (r:1 w:1)
+	// Storage: FastUnstake Head (r:1 w:0)
+	// Storage: FastUnstake CounterForQueue (r:1 w:1)
+	fn deregister() -> Weight {
+		Weight::from_ref_time(15_000_000 as u64)
+			.saturating_add(RocksDbWeight::get().reads(4 as u64))
+			.saturating_add(RocksDbWeight::get().writes(2 as u64))
+	}
+	// Storage: FastUnstake ErasToCheckPerBlock (r:0 w:1)
+	fn control() -> Weight {
+		Weight::from_ref_time(3_000_000 as u64)
+			.saturating_add(RocksDbWeight::get().writes(1 as u64))
+	}
+}
diff --git a/substrate/frame/nomination-pools/benchmarking/src/lib.rs b/substrate/frame/nomination-pools/benchmarking/src/lib.rs
index 040a9fa8e82..c31bcb1546e 100644
--- a/substrate/frame/nomination-pools/benchmarking/src/lib.rs
+++ b/substrate/frame/nomination-pools/benchmarking/src/lib.rs
@@ -52,12 +52,6 @@ pub trait Config:
 
 pub struct Pallet<T: Config>(Pools<T>);
 
-fn min_create_bond<T: Config>() -> BalanceOf<T> {
-	MinCreateBond::<T>::get()
-		.max(T::StakingInterface::minimum_bond())
-		.max(CurrencyOf::<T>::minimum_balance())
-}
-
 fn create_funded_user_with_balance<T: pallet_nomination_pools::Config>(
 	string: &'static str,
 	n: u32,
@@ -220,7 +214,7 @@ impl<T: Config> ListScenario<T> {
 
 frame_benchmarking::benchmarks! {
 	join {
-		let origin_weight = min_create_bond::<T>() * 2u32.into();
+		let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
 
 		// setup the worst case list scenario.
 		let scenario = ListScenario::<T>::new(origin_weight, true)?;
@@ -246,7 +240,7 @@ frame_benchmarking::benchmarks! {
 	}
 
 	bond_extra_transfer {
-		let origin_weight = min_create_bond::<T>() * 2u32.into();
+		let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
 		let scenario = ListScenario::<T>::new(origin_weight, true)?;
 		let extra = scenario.dest_weight - origin_weight;
 
@@ -261,7 +255,7 @@ frame_benchmarking::benchmarks! {
 	}
 
 	bond_extra_reward {
-		let origin_weight = min_create_bond::<T>() * 2u32.into();
+		let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
 		let scenario = ListScenario::<T>::new(origin_weight, true)?;
 		let extra = (scenario.dest_weight - origin_weight).max(CurrencyOf::<T>::minimum_balance());
 
@@ -279,7 +273,7 @@ frame_benchmarking::benchmarks! {
 	}
 
 	claim_payout {
-		let origin_weight = min_create_bond::<T>() * 2u32.into();
+		let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
 		let ed = CurrencyOf::<T>::minimum_balance();
 		let (depositor, pool_account) = create_pool_account::<T>(0, origin_weight);
 		let reward_account = Pools::<T>::create_reward_account(1);
@@ -309,7 +303,7 @@ frame_benchmarking::benchmarks! {
 	unbond {
 		// The weight the nominator will start at. The value used here is expected to be
 		// significantly higher than the first position in a list (e.g. the first bag threshold).
-		let origin_weight = min_create_bond::<T>() * 200u32.into();
+		let origin_weight = Pools::<T>::depositor_min_bond() * 200u32.into();
 		let scenario = ListScenario::<T>::new(origin_weight, false)?;
 		let amount = origin_weight - scenario.dest_weight;
 
@@ -340,7 +334,7 @@ frame_benchmarking::benchmarks! {
 	pool_withdraw_unbonded {
 		let s in 0 .. MAX_SPANS;
 
-		let min_create_bond = min_create_bond::<T>();
+		let min_create_bond = Pools::<T>::depositor_min_bond();
 		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond);
 
 		// Add a new member
@@ -382,7 +376,7 @@ frame_benchmarking::benchmarks! {
 	withdraw_unbonded_update {
 		let s in 0 .. MAX_SPANS;
 
-		let min_create_bond = min_create_bond::<T>();
+		let min_create_bond = Pools::<T>::depositor_min_bond();
 		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond);
 
 		// Add a new member
@@ -428,7 +422,7 @@ frame_benchmarking::benchmarks! {
 	withdraw_unbonded_kill {
 		let s in 0 .. MAX_SPANS;
 
-		let min_create_bond = min_create_bond::<T>();
+		let min_create_bond = Pools::<T>::depositor_min_bond();
 		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond);
 		let depositor_lookup = T::Lookup::unlookup(depositor.clone());
 
@@ -493,14 +487,14 @@ frame_benchmarking::benchmarks! {
 	}
 
 	create {
-		let min_create_bond = min_create_bond::<T>();
+		let min_create_bond = Pools::<T>::depositor_min_bond();
 		let depositor: T::AccountId = account("depositor", USER_SEED, 0);
 		let depositor_lookup = T::Lookup::unlookup(depositor.clone());
 
 		// Give the depositor some balance to bond
 		CurrencyOf::<T>::make_free_balance_be(&depositor, min_create_bond * 2u32.into());
 
-		// Make sure no pools exist as a pre-condition for our verify checks
+		// Make sure no Pools exist at a pre-condition for our verify checks
 		assert_eq!(RewardPools::<T>::count(), 0);
 		assert_eq!(BondedPools::<T>::count(), 0);
 
@@ -540,7 +534,7 @@ frame_benchmarking::benchmarks! {
 		let n in 1 .. T::MaxNominations::get();
 
 		// Create a pool
-		let min_create_bond = min_create_bond::<T>() * 2u32.into();
+		let min_create_bond = Pools::<T>::depositor_min_bond() * 2u32.into();
 		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond);
 
 		// Create some accounts to nominate. For the sake of benchmarking they don't need to be
@@ -577,7 +571,7 @@ frame_benchmarking::benchmarks! {
 
 	set_state {
 		// Create a pool
-		let min_create_bond = min_create_bond::<T>();
+		let min_create_bond = Pools::<T>::depositor_min_bond();
 		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond);
 		BondedPools::<T>::mutate(&1, |maybe_pool| {
 			// Force the pool into an invalid state
@@ -595,7 +589,7 @@ frame_benchmarking::benchmarks! {
 		let n in 1 .. <T as pallet_nomination_pools::Config>::MaxMetadataLen::get();
 
 		// Create a pool
-		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond::<T>() * 2u32.into());
+		let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into());
 
 		// Create metadata of the max possible size
 		let metadata: Vec<u8> = (0..n).map(|_| 42).collect();
@@ -624,7 +618,7 @@ frame_benchmarking::benchmarks! {
 
 	update_roles {
 		let first_id = pallet_nomination_pools::LastPoolId::<T>::get() + 1;
-		let (root, _) = create_pool_account::<T>(0, min_create_bond::<T>() * 2u32.into());
+		let (root, _) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into());
 		let random: T::AccountId = account("but is anything really random in computers..?", 0, USER_SEED);
 	}:_(
 		RuntimeOrigin::Signed(root.clone()),
@@ -646,7 +640,7 @@ frame_benchmarking::benchmarks! {
 
 	chill {
 		// Create a pool
-		let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond::<T>() * 2u32.into());
+		let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into());
 
 		// Nominate with the pool.
 		 let validators: Vec<_> = (0..T::MaxNominations::get())
diff --git a/substrate/frame/nomination-pools/src/lib.rs b/substrate/frame/nomination-pools/src/lib.rs
index dfe4bf19310..28d10ce5734 100644
--- a/substrate/frame/nomination-pools/src/lib.rs
+++ b/substrate/frame/nomination-pools/src/lib.rs
@@ -2184,10 +2184,11 @@ impl<T: Config> Pallet<T> {
 	///
 	/// It is essentially `max { MinNominatorBond, MinCreateBond, MinJoinBond }`, where the former
 	/// is coming from the staking pallet and the latter two are configured in this pallet.
-	fn depositor_min_bond() -> BalanceOf<T> {
+	pub fn depositor_min_bond() -> BalanceOf<T> {
 		T::StakingInterface::minimum_bond()
 			.max(MinCreateBond::<T>::get())
 			.max(MinJoinBond::<T>::get())
+			.max(T::Currency::minimum_balance())
 	}
 	/// Remove everything related to the given bonded pool.
 	///
diff --git a/substrate/frame/nomination-pools/src/tests.rs b/substrate/frame/nomination-pools/src/tests.rs
index 7b8d14164d6..5074a7ffa69 100644
--- a/substrate/frame/nomination-pools/src/tests.rs
+++ b/substrate/frame/nomination-pools/src/tests.rs
@@ -2771,7 +2771,7 @@ mod unbond {
 
 	#[test]
 	fn partial_unbond_era_tracking() {
-		ExtBuilder::default().build_and_execute(|| {
+		ExtBuilder::default().ed(1).build_and_execute(|| {
 			// to make the depositor capable of withdrawing.
 			StakingMinBond::set(1);
 			MinCreateBond::<T>::set(1);
diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs
index 56fcb1965bc..df568d6b596 100644
--- a/substrate/frame/staking/src/lib.rs
+++ b/substrate/frame/staking/src/lib.rs
@@ -563,7 +563,7 @@ impl<T: Config> StakingLedger<T> {
 	///
 	/// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash was
 	/// applied.
-	fn slash(
+	pub fn slash(
 		&mut self,
 		slash_amount: BalanceOf<T>,
 		minimum_balance: BalanceOf<T>,
diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs
index abb8649169f..399f50aaed8 100644
--- a/substrate/frame/staking/src/pallet/impls.rs
+++ b/substrate/frame/staking/src/pallet/impls.rs
@@ -653,10 +653,10 @@ impl<T: Config> Pallet<T> {
 	#[cfg(feature = "runtime-benchmarks")]
 	pub fn add_era_stakers(
 		current_era: EraIndex,
-		controller: T::AccountId,
+		stash: T::AccountId,
 		exposure: Exposure<T::AccountId, BalanceOf<T>>,
 	) {
-		<ErasStakers<T>>::insert(&current_era, &controller, &exposure);
+		<ErasStakers<T>>::insert(&current_era, &stash, &exposure);
 	}
 
 	#[cfg(feature = "runtime-benchmarks")]
diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs
index d113c0bb224..4db3870c62d 100644
--- a/substrate/frame/staking/src/pallet/mod.rs
+++ b/substrate/frame/staking/src/pallet/mod.rs
@@ -504,6 +504,7 @@ pub mod pallet {
 
 	/// Slashing spans for stash accounts.
 	#[pallet::storage]
+	#[pallet::getter(fn slashing_spans)]
 	#[pallet::unbounded]
 	pub(crate) type SlashingSpans<T: Config> =
 		StorageMap<_, Twox64Concat, T::AccountId, slashing::SlashingSpans>;
@@ -656,8 +657,8 @@ pub mod pallet {
 		EraPaid(EraIndex, BalanceOf<T>, BalanceOf<T>),
 		/// The nominator has been rewarded by this amount. \[stash, amount\]
 		Rewarded(T::AccountId, BalanceOf<T>),
-		/// One validator (and its nominators) has been slashed by the given amount.
-		/// \[validator, amount\]
+		/// One staker (and potentially its nominators) has been slashed by the given amount.
+		/// \[staker, amount\]
 		Slashed(T::AccountId, BalanceOf<T>),
 		/// An old slashing report from a prior era was discarded because it could
 		/// not be processed. \[session_index\]
-- 
GitLab