From 113727950b5217e2f16b6a69b8d76acc27ecca3b Mon Sep 17 00:00:00 2001
From: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Date: Mon, 12 Sep 2022 15:27:11 +0100
Subject: [PATCH] Fuzz testing for nomination pools (#12002)

* some additional tests and stuff

* make sanity public

* add some sort of fuzz test for pools

* breaks every now and then

* breaks every now and then

* IT WORKS AND PASSES 100k TESTS

* cleanup

* safe id addition

* fix assert_eq_error_rate

* Update frame/nomination-pools/src/tests.rs

Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Update frame/nomination-pools/src/tests.rs

Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* add some doc

* Fix

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

Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: command-bot <>
---
 substrate/Cargo.lock                          |  77 ++-
 substrate/frame/nomination-pools/Cargo.toml   |   2 +
 substrate/frame/nomination-pools/src/lib.rs   |  49 +-
 substrate/frame/nomination-pools/src/mock.rs  |   3 +-
 substrate/frame/nomination-pools/src/tests.rs | 537 ++++++++++++++++++
 substrate/frame/support/src/traits/misc.rs    |  15 +-
 substrate/primitives/runtime/src/lib.rs       |  21 +-
 .../benchmarking-cli/src/machine/hardware.rs  |   8 +-
 8 files changed, 647 insertions(+), 65 deletions(-)

diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock
index f47ac8ba408..335e8b35267 100644
--- a/substrate/Cargo.lock
+++ b/substrate/Cargo.lock
@@ -905,7 +905,7 @@ dependencies = [
  "ansi_term",
  "clap 3.1.18",
  "node-cli",
- "rand 0.8.4",
+ "rand 0.8.5",
  "sc-chain-spec",
  "sc-keystore",
  "sp-core",
@@ -2038,7 +2038,7 @@ dependencies = [
  "num-traits",
  "parity-scale-codec",
  "parking_lot 0.12.1",
- "rand 0.8.4",
+ "rand 0.8.5",
  "scale-info",
 ]
 
@@ -2049,7 +2049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
 dependencies = [
  "byteorder",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rustc-hex",
  "static_assertions",
 ]
@@ -2143,7 +2143,7 @@ dependencies = [
  "log",
  "memory-db",
  "parity-scale-codec",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rand_pcg 0.3.1",
  "sc-block-builder",
  "sc-cli",
@@ -2215,7 +2215,7 @@ dependencies = [
  "frame-support",
  "honggfuzz",
  "parity-scale-codec",
- "rand 0.8.4",
+ "rand 0.8.5",
  "scale-info",
  "sp-arithmetic",
  "sp-npos-elections",
@@ -3212,7 +3212,7 @@ dependencies = [
  "jsonrpsee-types",
  "lazy_static",
  "parking_lot 0.12.1",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rustc-hash",
  "serde",
  "serde_json",
@@ -3593,7 +3593,7 @@ dependencies = [
  "log",
  "prost",
  "prost-build",
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -3620,7 +3620,7 @@ dependencies = [
  "pin-project",
  "prost",
  "prost-build",
- "rand 0.8.4",
+ "rand 0.8.5",
  "ring",
  "rw-stream-sink",
  "sha2 0.10.2",
@@ -3767,7 +3767,7 @@ dependencies = [
  "libp2p-core",
  "libp2p-swarm",
  "log",
- "rand 0.8.4",
+ "rand 0.8.5",
  "smallvec",
  "socket2",
  "void",
@@ -3821,7 +3821,7 @@ dependencies = [
  "log",
  "prost",
  "prost-build",
- "rand 0.8.4",
+ "rand 0.8.5",
  "sha2 0.10.2",
  "snow",
  "static_assertions",
@@ -3895,7 +3895,7 @@ dependencies = [
  "prost",
  "prost-build",
  "prost-codec",
- "rand 0.8.4",
+ "rand 0.8.5",
  "smallvec",
  "static_assertions",
  "thiserror",
@@ -3918,7 +3918,7 @@ dependencies = [
  "log",
  "prost",
  "prost-build",
- "rand 0.8.4",
+ "rand 0.8.5",
  "sha2 0.10.2",
  "thiserror",
  "unsigned-varint",
@@ -4076,7 +4076,7 @@ dependencies = [
  "libsecp256k1-core",
  "libsecp256k1-gen-ecmult",
  "libsecp256k1-gen-genmult",
- "rand 0.8.4",
+ "rand 0.8.5",
  "serde",
  "sha2 0.9.8",
  "typenum",
@@ -4502,7 +4502,7 @@ dependencies = [
  "num-complex",
  "num-rational 0.4.0",
  "num-traits",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rand_distr",
  "simba",
  "typenum",
@@ -4525,7 +4525,7 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e7d66043b25d4a6cccb23619d10c19c25304b355a7dccd4a8e11423dd2382146"
 dependencies = [
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -4685,7 +4685,7 @@ dependencies = [
  "pallet-transaction-payment",
  "parity-scale-codec",
  "platforms",
- "rand 0.8.4",
+ "rand 0.8.5",
  "regex",
  "remote-externalities",
  "sc-authority-discovery",
@@ -5341,7 +5341,7 @@ dependencies = [
  "frame-election-provider-support",
  "honggfuzz",
  "pallet-bags-list",
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -5495,7 +5495,7 @@ dependencies = [
  "pallet-utility",
  "parity-scale-codec",
  "pretty_assertions",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rand_pcg 0.3.1",
  "scale-info",
  "serde",
@@ -5931,6 +5931,7 @@ dependencies = [
  "log",
  "pallet-balances",
  "parity-scale-codec",
+ "rand 0.8.5",
  "scale-info",
  "sp-core",
  "sp-io",
@@ -6554,7 +6555,7 @@ dependencies = [
  "lz4",
  "memmap2 0.2.1",
  "parking_lot 0.11.2",
- "rand 0.8.4",
+ "rand 0.8.5",
  "snap",
 ]
 
@@ -7168,7 +7169,7 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
 dependencies = [
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -7207,20 +7208,19 @@ dependencies = [
  "libc",
  "rand_chacha 0.2.2",
  "rand_core 0.5.1",
- "rand_hc 0.2.0",
+ "rand_hc",
  "rand_pcg 0.2.1",
 ]
 
 [[package]]
 name = "rand"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
  "rand_chacha 0.3.0",
  "rand_core 0.6.2",
- "rand_hc 0.3.0",
 ]
 
 [[package]]
@@ -7268,7 +7268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
 dependencies = [
  "num-traits",
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -7280,15 +7280,6 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
-[[package]]
-name = "rand_hc"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
-dependencies = [
- "rand_core 0.6.2",
-]
-
 [[package]]
 name = "rand_pcg"
 version = "0.2.1"
@@ -7907,7 +7898,7 @@ dependencies = [
  "parity-scale-codec",
  "parking_lot 0.12.1",
  "quickcheck",
- "rand 0.8.4",
+ "rand 0.8.5",
  "sc-client-api",
  "sc-state-db",
  "sp-arithmetic",
@@ -8279,7 +8270,7 @@ dependencies = [
  "log",
  "parity-scale-codec",
  "parking_lot 0.12.1",
- "rand 0.8.4",
+ "rand 0.8.5",
  "sc-block-builder",
  "sc-chain-spec",
  "sc-client-api",
@@ -9397,7 +9388,7 @@ dependencies = [
  "futures",
  "httparse",
  "log",
- "rand 0.8.4",
+ "rand 0.8.5",
  "sha-1 0.9.4",
 ]
 
@@ -9877,7 +9868,7 @@ dependencies = [
  "clap 3.1.18",
  "honggfuzz",
  "parity-scale-codec",
- "rand 0.8.4",
+ "rand 0.8.5",
  "scale-info",
  "sp-npos-elections",
  "sp-runtime",
@@ -10282,7 +10273,7 @@ dependencies = [
  "lazy_static",
  "nalgebra",
  "num-traits",
- "rand 0.8.4",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -11053,7 +11044,7 @@ dependencies = [
  "ipnet",
  "lazy_static",
  "log",
- "rand 0.8.4",
+ "rand 0.8.5",
  "smallvec",
  "thiserror",
  "tinyvec",
@@ -11141,7 +11132,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
 dependencies = [
  "cfg-if 1.0.0",
  "digest 0.10.3",
- "rand 0.8.4",
+ "rand 0.7.3",
  "static_assertions",
 ]
 
@@ -11859,7 +11850,7 @@ dependencies = [
  "memfd",
  "memoffset",
  "paste 1.0.6",
- "rand 0.8.4",
+ "rand 0.8.5",
  "rustix",
  "thiserror",
  "wasmtime-asm-macros",
@@ -12151,7 +12142,7 @@ dependencies = [
  "log",
  "nohash-hasher",
  "parking_lot 0.12.1",
- "rand 0.8.4",
+ "rand 0.8.5",
  "static_assertions",
 ]
 
diff --git a/substrate/frame/nomination-pools/Cargo.toml b/substrate/frame/nomination-pools/Cargo.toml
index 1d613511eac..ef8465e670b 100644
--- a/substrate/frame/nomination-pools/Cargo.toml
+++ b/substrate/frame/nomination-pools/Cargo.toml
@@ -29,10 +29,12 @@ log = { version = "0.4.0", default-features = false }
 [dev-dependencies]
 pallet-balances = { version = "4.0.0-dev", path = "../balances" }
 sp-tracing = { version = "5.0.0", path = "../../primitives/tracing" }
+rand = { version = "0.8.5", features = ["small_rng"] }
 
 [features]
 runtime-benchmarks = []
 try-runtime = [ "frame-support/try-runtime" ]
+fuzz-test = []
 default = ["std"]
 std = [
 	"codec/std",
diff --git a/substrate/frame/nomination-pools/src/lib.rs b/substrate/frame/nomination-pools/src/lib.rs
index a8c4e50daa0..9e872955669 100644
--- a/substrate/frame/nomination-pools/src/lib.rs
+++ b/substrate/frame/nomination-pools/src/lib.rs
@@ -1453,7 +1453,7 @@ pub mod pallet {
 		PartialUnbondNotAllowedPermissionlessly,
 	}
 
-	#[derive(Encode, Decode, PartialEq, TypeInfo, frame_support::PalletError)]
+	#[derive(Encode, Decode, PartialEq, TypeInfo, frame_support::PalletError, RuntimeDebug)]
 	pub enum DefensiveError {
 		/// There isn't enough space in the unbond pool.
 		NotEnoughSpaceInUnbondPool,
@@ -1758,8 +1758,8 @@ pub mod pallet {
 
 			let bonded_pool = BondedPool::<T>::get(member.pool_id)
 				.defensive_ok_or::<Error<T>>(DefensiveError::PoolNotFound.into())?;
-			let mut sub_pools = SubPoolsStorage::<T>::get(member.pool_id)
-				.defensive_ok_or::<Error<T>>(DefensiveError::SubPoolsNotFound.into())?;
+			let mut sub_pools =
+				SubPoolsStorage::<T>::get(member.pool_id).ok_or(Error::<T>::SubPoolsNotFound)?;
 
 			bonded_pool.ok_to_withdraw_unbonded_with(&caller, &member_account)?;
 
@@ -1887,10 +1887,10 @@ pub mod pallet {
 			);
 			ensure!(!PoolMembers::<T>::contains_key(&who), Error::<T>::AccountBelongsToOtherPool);
 
-			let pool_id = LastPoolId::<T>::mutate(|id| {
-				*id += 1;
-				*id
-			});
+			let pool_id = LastPoolId::<T>::try_mutate::<_, Error<T>, _>(|id| {
+				*id = id.checked_add(1).ok_or(Error::<T>::OverflowRisk)?;
+				Ok(*id)
+			})?;
 			let mut bonded_pool = BondedPool::<T>::new(
 				pool_id,
 				PoolRoles {
@@ -2416,16 +2416,46 @@ impl<T: Config> Pallet<T> {
 
 		for id in reward_pools {
 			let account = Self::create_reward_account(id);
-			assert!(T::Currency::free_balance(&account) >= T::Currency::minimum_balance());
+			assert!(
+				T::Currency::free_balance(&account) >= T::Currency::minimum_balance(),
+				"reward pool of {id}: {:?} (ed = {:?})",
+				T::Currency::free_balance(&account),
+				T::Currency::minimum_balance()
+			);
 		}
 
 		let mut pools_members = BTreeMap::<PoolId, u32>::new();
+		let mut pools_members_pending_rewards = BTreeMap::<PoolId, BalanceOf<T>>::new();
 		let mut all_members = 0u32;
 		PoolMembers::<T>::iter().for_each(|(_, d)| {
-			assert!(BondedPools::<T>::contains_key(d.pool_id));
+			let bonded_pool = BondedPools::<T>::get(d.pool_id).unwrap();
 			assert!(!d.total_points().is_zero(), "no member should have zero points: {:?}", d);
 			*pools_members.entry(d.pool_id).or_default() += 1;
 			all_members += 1;
+
+			let reward_pool = RewardPools::<T>::get(d.pool_id).unwrap();
+			if !bonded_pool.points.is_zero() {
+				let current_rc =
+					reward_pool.current_reward_counter(d.pool_id, bonded_pool.points).unwrap();
+				*pools_members_pending_rewards.entry(d.pool_id).or_default() +=
+					d.pending_rewards(current_rc).unwrap();
+			} // else this pool has been heavily slashed and cannot have any rewards anymore.
+		});
+
+		RewardPools::<T>::iter_keys().for_each(|id| {
+			// the sum of the pending rewards must be less than the leftover balance. Since the
+			// reward math rounds down, we might accumulate some dust here.
+			log!(
+				trace,
+				"pool {:?}, sum pending rewards = {:?}, remaining balance = {:?}",
+				id,
+				pools_members_pending_rewards.get(&id),
+				RewardPool::<T>::current_balance(id)
+			);
+			assert!(
+				RewardPool::<T>::current_balance(id) >=
+					pools_members_pending_rewards.get(&id).map(|x| *x).unwrap_or_default()
+			)
 		});
 
 		BondedPools::<T>::iter().for_each(|(id, inner)| {
@@ -2470,6 +2500,7 @@ impl<T: Config> Pallet<T> {
 				sum_unbonding_balance
 			);
 		}
+
 		Ok(())
 	}
 
diff --git a/substrate/frame/nomination-pools/src/mock.rs b/substrate/frame/nomination-pools/src/mock.rs
index 4428c28558f..f1574b3684a 100644
--- a/substrate/frame/nomination-pools/src/mock.rs
+++ b/substrate/frame/nomination-pools/src/mock.rs
@@ -4,6 +4,7 @@ use frame_support::{assert_ok, parameter_types, PalletId};
 use frame_system::RawOrigin;
 use sp_runtime::FixedU128;
 
+pub type BlockNumber = u64;
 pub type AccountId = u128;
 pub type Balance = u128;
 pub type RewardCounter = FixedU128;
@@ -129,7 +130,7 @@ impl frame_system::Config for Runtime {
 	type BaseCallFilter = frame_support::traits::Everything;
 	type Origin = Origin;
 	type Index = u64;
-	type BlockNumber = u64;
+	type BlockNumber = BlockNumber;
 	type Call = Call;
 	type Hash = sp_core::H256;
 	type Hashing = sp_runtime::traits::BlakeTwo256;
diff --git a/substrate/frame/nomination-pools/src/tests.rs b/substrate/frame/nomination-pools/src/tests.rs
index 20ba2b76fe3..362fece3f3d 100644
--- a/substrate/frame/nomination-pools/src/tests.rs
+++ b/substrate/frame/nomination-pools/src/tests.rs
@@ -1774,6 +1774,54 @@ mod claim_payout {
 		})
 	}
 
+	#[test]
+	fn bond_extra_pending_rewards_works() {
+		ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| {
+			MaxPoolMembers::<Runtime>::set(None);
+			MaxPoolMembersPerPool::<Runtime>::set(None);
+
+			// pool receives some rewards.
+			Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap();
+			System::reset_events();
+
+			// 10 cashes it out, and bonds it.
+			{
+				assert_ok!(Pools::claim_payout(Origin::signed(10)));
+				let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap();
+				// there is 30 points and 30 reward points in the system RC is 1.
+				assert_eq!(member.last_recorded_reward_counter, 1.into());
+				assert_eq!(reward_pool.total_rewards_claimed, 10);
+				// these two are not updated -- only updated when the points change.
+				assert_eq!(reward_pool.last_recorded_total_payouts, 0);
+				assert_eq!(reward_pool.last_recorded_reward_counter, 0.into());
+
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 }]
+				);
+			}
+
+			// 20 re-bonds it.
+			{
+				assert_ok!(Pools::bond_extra(Origin::signed(20), BondExtra::Rewards));
+				let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap();
+				assert_eq!(member.last_recorded_reward_counter, 1.into());
+				assert_eq!(reward_pool.total_rewards_claimed, 30);
+				// since points change, these two are updated.
+				assert_eq!(reward_pool.last_recorded_total_payouts, 30);
+				assert_eq!(reward_pool.last_recorded_reward_counter, 1.into());
+
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![
+						Event::PaidOut { member: 20, pool_id: 1, payout: 20 },
+						Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: false }
+					]
+				);
+			}
+		})
+	}
+
 	#[test]
 	fn unbond_updates_recorded_data() {
 		ExtBuilder::default()
@@ -3733,6 +3781,170 @@ mod withdraw_unbonded {
 		})
 	}
 
+	#[test]
+	fn out_of_sync_unbonding_chunks() {
+		// the unbonding_eras in pool member are always fixed to the era at which they are unlocked,
+		// but the actual unbonding pools get pruned and might get combined in the no_era pool.
+		// Pools are only merged when one unbonds, so we unbond a little bit on every era to
+		// simulate this.
+		ExtBuilder::default()
+			.add_members(vec![(20, 100), (30, 100)])
+			.build_and_execute(|| {
+				System::reset_events();
+
+				// when
+				assert_ok!(Pools::unbond(Origin::signed(20), 20, 5));
+				assert_ok!(Pools::unbond(Origin::signed(30), 30, 5));
+
+				// then member-local unbonding is pretty much in sync with the global pools.
+				assert_eq!(
+					PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras,
+					member_unbonding_eras!(3 => 5)
+				);
+				assert_eq!(
+					PoolMembers::<Runtime>::get(30).unwrap().unbonding_eras,
+					member_unbonding_eras!(3 => 5)
+				);
+				assert_eq!(
+					SubPoolsStorage::<Runtime>::get(1).unwrap(),
+					SubPools {
+						no_era: Default::default(),
+						with_era: unbonding_pools_with_era! {
+							3 => UnbondPool { points: 10, balance: 10 }
+						}
+					}
+				);
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![
+						Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 3 },
+						Event::Unbonded { member: 30, pool_id: 1, points: 5, balance: 5, era: 3 },
+					]
+				);
+
+				// when
+				CurrentEra::set(1);
+				assert_ok!(Pools::unbond(Origin::signed(20), 20, 5));
+
+				// then still member-local unbonding is pretty much in sync with the global pools.
+				assert_eq!(
+					PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras,
+					member_unbonding_eras!(3 => 5, 4 => 5)
+				);
+				assert_eq!(
+					SubPoolsStorage::<Runtime>::get(1).unwrap(),
+					SubPools {
+						no_era: Default::default(),
+						with_era: unbonding_pools_with_era! {
+							3 => UnbondPool { points: 10, balance: 10 },
+							4 => UnbondPool { points: 5, balance: 5 }
+						}
+					}
+				);
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 4 }]
+				);
+
+				// when
+				CurrentEra::set(2);
+				assert_ok!(Pools::unbond(Origin::signed(20), 20, 5));
+
+				// then still member-local unbonding is pretty much in sync with the global pools.
+				assert_eq!(
+					PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras,
+					member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5)
+				);
+				assert_eq!(
+					SubPoolsStorage::<Runtime>::get(1).unwrap(),
+					SubPools {
+						no_era: Default::default(),
+						with_era: unbonding_pools_with_era! {
+							3 => UnbondPool { points: 10, balance: 10 },
+							4 => UnbondPool { points: 5, balance: 5 },
+							5 => UnbondPool { points: 5, balance: 5 }
+						}
+					}
+				);
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 }]
+				);
+
+				// when
+				CurrentEra::set(5);
+				assert_ok!(Pools::unbond(Origin::signed(20), 20, 5));
+
+				// then
+				assert_eq!(
+					PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras,
+					member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5, 8 => 5)
+				);
+				assert_eq!(
+					SubPoolsStorage::<Runtime>::get(1).unwrap(),
+					SubPools {
+						// era 3 is merged into no_era.
+						no_era: UnbondPool { points: 10, balance: 10 },
+						with_era: unbonding_pools_with_era! {
+							4 => UnbondPool { points: 5, balance: 5 },
+							5 => UnbondPool { points: 5, balance: 5 },
+							8 => UnbondPool { points: 5, balance: 5 }
+						}
+					}
+				);
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 8 }]
+				);
+
+				// now we start withdrawing unlocked bonds.
+
+				// when
+				assert_ok!(Pools::withdraw_unbonded(Origin::signed(20), 20, 0));
+				// then
+				assert_eq!(
+					PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras,
+					member_unbonding_eras!(8 => 5)
+				);
+				assert_eq!(
+					SubPoolsStorage::<Runtime>::get(1).unwrap(),
+					SubPools {
+						// era 3 is merged into no_era.
+						no_era: UnbondPool { points: 5, balance: 5 },
+						with_era: unbonding_pools_with_era! {
+							8 => UnbondPool { points: 5, balance: 5 }
+						}
+					}
+				);
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![Event::Withdrawn { member: 20, pool_id: 1, points: 15, balance: 15 }]
+				);
+
+				// when
+				assert_ok!(Pools::withdraw_unbonded(Origin::signed(30), 30, 0));
+				// then
+				assert_eq!(
+					PoolMembers::<Runtime>::get(30).unwrap().unbonding_eras,
+					member_unbonding_eras!()
+				);
+				assert_eq!(
+					SubPoolsStorage::<Runtime>::get(1).unwrap(),
+					SubPools {
+						// era 3 is merged into no_era.
+						no_era: Default::default(),
+						with_era: unbonding_pools_with_era! {
+							8 => UnbondPool { points: 5, balance: 5 }
+						}
+					}
+				);
+				assert_eq!(
+					pool_events_since_last_call(),
+					vec![Event::Withdrawn { member: 30, pool_id: 1, points: 5, balance: 5 }]
+				);
+			})
+	}
+
 	#[test]
 	fn full_multi_step_withdrawing_depositor() {
 		ExtBuilder::default().ed(1).build_and_execute(|| {
@@ -4768,3 +4980,328 @@ mod reward_counter_precision {
 			});
 	}
 }
+
+// NOTE: run this with debug_assertions, but in release mode.
+#[cfg(feature = "fuzz-test")]
+mod fuzz_test {
+	use super::*;
+	use crate::pallet::{Call as PoolsCall, Event as PoolsEvents};
+	use frame_support::traits::UnfilteredDispatchable;
+	use rand::{seq::SliceRandom, thread_rng, Rng};
+	use sp_runtime::{assert_eq_error_rate, Perquintill};
+
+	const ERA: BlockNumber = 1000;
+	const MAX_ED_MULTIPLE: Balance = 10_000;
+	const MIN_ED_MULTIPLE: Balance = 10;
+
+	// not quite elegant, just to make it available in random_signed_origin.
+	const REWARD_AGENT_ACCOUNT: AccountId = 42;
+
+	/// Grab random accounts, either known ones, or new ones.
+	fn random_signed_origin<R: Rng>(rng: &mut R) -> (Origin, AccountId) {
+		let count = PoolMembers::<T>::count();
+		if rng.gen::<bool>() && count > 0 {
+			// take an existing account.
+			let skip = rng.gen_range(0..count as usize);
+
+			// this is tricky: the account might be our reward agent, which we never want to be
+			// randomly chosen here. Try another one, or, if it is only our agent, return a random
+			// one nonetheless.
+			let candidate = PoolMembers::<T>::iter_keys().skip(skip).take(1).next().unwrap();
+			let acc =
+				if candidate == REWARD_AGENT_ACCOUNT { rng.gen::<AccountId>() } else { candidate };
+
+			(Origin::signed(acc), acc)
+		} else {
+			// create a new account
+			let acc = rng.gen::<AccountId>();
+			(Origin::signed(acc), acc)
+		}
+	}
+
+	fn random_ed_multiple<R: Rng>(rng: &mut R) -> Balance {
+		let multiple = rng.gen_range(MIN_ED_MULTIPLE..MAX_ED_MULTIPLE);
+		ExistentialDeposit::get() * multiple
+	}
+
+	fn fund_account<R: Rng>(rng: &mut R, account: &AccountId) {
+		let target_amount = random_ed_multiple(rng);
+		if let Some(top_up) = target_amount.checked_sub(Balances::free_balance(account)) {
+			let _ = Balances::deposit_creating(account, top_up);
+		}
+		assert!(Balances::free_balance(account) >= target_amount);
+	}
+
+	fn random_existing_pool<R: Rng>(mut rng: &mut R) -> Option<PoolId> {
+		BondedPools::<T>::iter_keys().collect::<Vec<_>>().choose(&mut rng).map(|x| *x)
+	}
+
+	fn random_call<R: Rng>(mut rng: &mut R) -> (crate::pallet::Call<T>, Origin) {
+		let op = rng.gen::<usize>();
+		let mut op_count =
+			<crate::pallet::Call<T> as frame_support::dispatch::GetCallName>::get_call_names()
+				.len();
+		// Exclude set_state, set_metadata, set_configs, update_roles and chill.
+		op_count -= 5;
+
+		match op % op_count {
+			0 => {
+				// join
+				let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
+				let (origin, who) = random_signed_origin(&mut rng);
+				fund_account(&mut rng, &who);
+				let amount = random_ed_multiple(&mut rng);
+				(PoolsCall::<T>::join { amount, pool_id }, origin)
+			},
+			1 => {
+				// bond_extra
+				let (origin, who) = random_signed_origin(&mut rng);
+				let extra = if rng.gen::<bool>() {
+					BondExtra::Rewards
+				} else {
+					fund_account(&mut rng, &who);
+					let amount = random_ed_multiple(&mut rng);
+					BondExtra::FreeBalance(amount)
+				};
+				(PoolsCall::<T>::bond_extra { extra }, origin)
+			},
+			2 => {
+				// claim_payout
+				let (origin, _) = random_signed_origin(&mut rng);
+				(PoolsCall::<T>::claim_payout {}, origin)
+			},
+			3 => {
+				// unbond
+				let (origin, who) = random_signed_origin(&mut rng);
+				let amount = random_ed_multiple(&mut rng);
+				(PoolsCall::<T>::unbond { member_account: who, unbonding_points: amount }, origin)
+			},
+			4 => {
+				// pool_withdraw_unbonded
+				let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
+				let (origin, _) = random_signed_origin(&mut rng);
+				(PoolsCall::<T>::pool_withdraw_unbonded { pool_id, num_slashing_spans: 0 }, origin)
+			},
+			5 => {
+				// withdraw_unbonded
+				let (origin, who) = random_signed_origin(&mut rng);
+				(
+					PoolsCall::<T>::withdraw_unbonded {
+						member_account: who,
+						num_slashing_spans: 0,
+					},
+					origin,
+				)
+			},
+			6 => {
+				// create
+				let (origin, who) = random_signed_origin(&mut rng);
+				let amount = random_ed_multiple(&mut rng);
+				fund_account(&mut rng, &who);
+				let root = who.clone();
+				let state_toggler = who.clone();
+				let nominator = who.clone();
+				(PoolsCall::<T>::create { amount, root, state_toggler, nominator }, origin)
+			},
+			7 => {
+				// nominate
+				let (origin, _) = random_signed_origin(&mut rng);
+				let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
+				let validators = Default::default();
+				(PoolsCall::<T>::nominate { pool_id, validators }, origin)
+			},
+			_ => unreachable!(),
+		}
+	}
+
+	#[derive(Default)]
+	struct RewardAgent {
+		who: AccountId,
+		pool_id: Option<PoolId>,
+		expected_reward: Balance,
+	}
+
+	// TODO: inject some slashes into the game.
+	impl RewardAgent {
+		fn new(who: AccountId) -> Self {
+			Self { who, ..Default::default() }
+		}
+
+		fn join(&mut self) {
+			if self.pool_id.is_some() {
+				return
+			}
+			let pool_id = LastPoolId::<T>::get();
+			let amount = 10 * ExistentialDeposit::get();
+			let origin = Origin::signed(self.who);
+			let _ = Balances::deposit_creating(&self.who, 10 * amount);
+			self.pool_id = Some(pool_id);
+			log::info!(target: "reward-agent", "🤖 reward agent joining in {} with {}", pool_id, amount);
+			assert_ok!(PoolsCall::join::<T> { amount, pool_id }.dispatch_bypass_filter(origin));
+		}
+
+		fn claim_payout(&mut self) {
+			// 10 era later, we claim our payout. We expect our income to be roughly what we
+			// calculated.
+			if !PoolMembers::<T>::contains_key(&self.who) {
+				log!(warn, "reward agent is not in the pool yet, cannot claim");
+				return
+			}
+			let pre = Balances::free_balance(&42);
+			let origin = Origin::signed(42);
+			assert_ok!(PoolsCall::<T>::claim_payout {}.dispatch_bypass_filter(origin));
+			let post = Balances::free_balance(&42);
+
+			let income = post - pre;
+			log::info!(
+				target: "reward-agent", "🤖 CLAIM: actual: {}, expected: {}",
+				income,
+				self.expected_reward,
+			);
+			assert_eq_error_rate!(income, self.expected_reward, 10);
+			self.expected_reward = 0;
+		}
+	}
+
+	#[test]
+	fn fuzz_test() {
+		let mut reward_agent = RewardAgent::new(42);
+		sp_tracing::try_init_simple();
+		// NOTE: use this to get predictable (non)randomness:
+		// use::{rngs::SmallRng, SeedableRng};
+		// let mut rng = SmallRng::from_seed([0u8; 32]);
+		let mut rng = thread_rng();
+		let mut ext = sp_io::TestExternalities::new_empty();
+		// NOTE: sadly events don't fulfill the requirements of hashmap or btreemap.
+		let mut events_histogram = Vec::<(PoolsEvents<T>, u32)>::default();
+		let mut iteration = 0 as BlockNumber;
+		let mut ok = 0;
+		let mut err = 0;
+
+		ext.execute_with(|| {
+			MaxPoolMembers::<T>::set(Some(10_000));
+			MaxPoolMembersPerPool::<T>::set(Some(1000));
+			MaxPools::<T>::set(Some(1_000));
+
+			MinCreateBond::<T>::set(10 * ExistentialDeposit::get());
+			MinJoinBond::<T>::set(5 * ExistentialDeposit::get());
+			System::set_block_number(1);
+		});
+
+		ExistentialDeposit::set(10u128.pow(12u32));
+		BondingDuration::set(8);
+
+		loop {
+			ext.execute_with(|| {
+				iteration += 1;
+				let (call, origin) = random_call(&mut rng);
+				let outcome = call.clone().dispatch_bypass_filter(origin.clone());
+
+				match outcome {
+					Ok(_) => ok += 1,
+					Err(_) => err += 1,
+				};
+
+				log!(
+					debug,
+					"iteration {}, call {:?}, origin {:?}, outcome: {:?}, so far {} ok {} err",
+					iteration,
+					call,
+					origin,
+					outcome,
+					ok,
+					err,
+				);
+
+				// possibly join the reward_agent
+				if iteration > ERA / 2 && BondedPools::<T>::count() > 0 {
+					reward_agent.join();
+				}
+				// and possibly roughly every 4 era, trigger payout for the agent. Doing this more
+				// frequent is also harmless.
+				if rng.gen_range(0..(4 * ERA)) == 0 {
+					reward_agent.claim_payout();
+				}
+
+				// execute sanity checks at a fixed interval, possibly on every block.
+				if iteration %
+					(std::env::var("SANITY_CHECK_INTERVAL")
+						.ok()
+						.and_then(|x| x.parse::<u64>().ok()))
+					.unwrap_or(1) == 0
+				{
+					log!(info, "running sanity checks at {}", iteration);
+					Pools::do_try_state(u8::MAX).unwrap();
+				}
+
+				// collect and reset events.
+				System::events()
+					.into_iter()
+					.map(|r| r.event)
+					.filter_map(
+						|e| if let mock::Event::Pools(inner) = e { Some(inner) } else { None },
+					)
+					.for_each(|e| {
+						if let Some((_, c)) = events_histogram
+							.iter_mut()
+							.find(|(x, _)| std::mem::discriminant(x) == std::mem::discriminant(&e))
+						{
+							*c += 1;
+						} else {
+							events_histogram.push((e, 1))
+						}
+					});
+				System::reset_events();
+
+				// trigger an era change, and check the status of the reward agent.
+				if iteration % ERA == 0 {
+					CurrentEra::mutate(|c| *c += 1);
+					BondedPools::<T>::iter().for_each(|(id, _)| {
+						let amount = random_ed_multiple(&mut rng);
+						let _ =
+							Balances::deposit_creating(&Pools::create_reward_account(id), amount);
+						// if we just paid out the reward agent, let's calculate how much we expect
+						// our reward agent to have earned.
+						if reward_agent.pool_id.map_or(false, |mid| mid == id) {
+							let all_points = BondedPool::<T>::get(id).map(|p| p.points).unwrap();
+							let member_points =
+								PoolMembers::<T>::get(reward_agent.who).map(|m| m.points).unwrap();
+							let agent_share = Perquintill::from_rational(member_points, all_points);
+							log::info!(
+								target: "reward-agent",
+								"🤖 REWARD = amount = {:?}, ratio: {:?}, share {:?}",
+								amount,
+								agent_share,
+								agent_share * amount,
+							);
+							reward_agent.expected_reward += agent_share * amount;
+						}
+					});
+
+					log!(
+						info,
+						"iteration {}, {} pools, {} members, {} ok {} err, events = {:?}",
+						iteration,
+						BondedPools::<T>::count(),
+						PoolMembers::<T>::count(),
+						ok,
+						err,
+						events_histogram
+							.iter()
+							.map(|(x, c)| (
+								format!("{:?}", x)
+									.split(" ")
+									.map(|x| x.to_string())
+									.collect::<Vec<_>>()
+									.first()
+									.cloned()
+									.unwrap(),
+								c,
+							))
+							.collect::<Vec<_>>(),
+					);
+				}
+			});
+		}
+	}
+}
diff --git a/substrate/frame/support/src/traits/misc.rs b/substrate/frame/support/src/traits/misc.rs
index 0f781d0bbd4..59d5ec067ba 100644
--- a/substrate/frame/support/src/traits/misc.rs
+++ b/substrate/frame/support/src/traits/misc.rs
@@ -151,10 +151,10 @@ pub trait DefensiveOption<T> {
 
 	/// Defensively transform this option to a result, mapping `None` to the return value of an
 	/// error closure.
-	fn defensive_ok_or_else<E, F: FnOnce() -> E>(self, err: F) -> Result<T, E>;
+	fn defensive_ok_or_else<E: sp_std::fmt::Debug, F: FnOnce() -> E>(self, err: F) -> Result<T, E>;
 
 	/// Defensively transform this option to a result, mapping `None` to a default value.
-	fn defensive_ok_or<E>(self, err: E) -> Result<T, E>;
+	fn defensive_ok_or<E: sp_std::fmt::Debug>(self, err: E) -> Result<T, E>;
 
 	/// Exactly the same as `map`, but it prints the appropriate warnings if the value being mapped
 	/// is `None`.
@@ -318,16 +318,17 @@ impl<T> DefensiveOption<T> for Option<T> {
 		)
 	}
 
-	fn defensive_ok_or_else<E, F: FnOnce() -> E>(self, err: F) -> Result<T, E> {
+	fn defensive_ok_or_else<E: sp_std::fmt::Debug, F: FnOnce() -> E>(self, err: F) -> Result<T, E> {
 		self.ok_or_else(|| {
-			defensive!();
-			err()
+			let err_value = err();
+			defensive!(err_value);
+			err_value
 		})
 	}
 
-	fn defensive_ok_or<E>(self, err: E) -> Result<T, E> {
+	fn defensive_ok_or<E: sp_std::fmt::Debug>(self, err: E) -> Result<T, E> {
 		self.ok_or_else(|| {
-			defensive!();
+			defensive!(err);
 			err
 		})
 	}
diff --git a/substrate/primitives/runtime/src/lib.rs b/substrate/primitives/runtime/src/lib.rs
index 7006a13e415..9c2b691f64f 100644
--- a/substrate/primitives/runtime/src/lib.rs
+++ b/substrate/primitives/runtime/src/lib.rs
@@ -36,6 +36,8 @@ pub use sp_std;
 
 #[doc(hidden)]
 pub use paste;
+#[doc(hidden)]
+pub use sp_arithmetic::traits::Saturating;
 
 #[doc(hidden)]
 pub use sp_application_crypto as app_crypto;
@@ -825,7 +827,24 @@ pub fn verify_encoded_lazy<V: Verify, T: codec::Encode>(
 macro_rules! assert_eq_error_rate {
 	($x:expr, $y:expr, $error:expr $(,)?) => {
 		assert!(
-			($x) >= (($y) - ($error)) && ($x) <= (($y) + ($error)),
+			($x >= $crate::Saturating::saturating_sub($y, $error)) &&
+				($x <= $crate::Saturating::saturating_add($y, $error)),
+			"{:?} != {:?} (with error rate {:?})",
+			$x,
+			$y,
+			$error,
+		);
+	};
+}
+
+/// Same as [`assert_eq_error_rate`], but intended to be used with floating point number, or
+/// generally those who do not have over/underflow potentials.
+#[macro_export]
+#[cfg(feature = "std")]
+macro_rules! assert_eq_error_rate_float {
+	($x:expr, $y:expr, $error:expr $(,)?) => {
+		assert!(
+			($x >= $y - $error) && ($x <= $y + $error),
 			"{:?} != {:?} (with error rate {:?})",
 			$x,
 			$y,
diff --git a/substrate/utils/frame/benchmarking-cli/src/machine/hardware.rs b/substrate/utils/frame/benchmarking-cli/src/machine/hardware.rs
index 5c62660cc7c..97960de99c4 100644
--- a/substrate/utils/frame/benchmarking-cli/src/machine/hardware.rs
+++ b/substrate/utils/frame/benchmarking-cli/src/machine/hardware.rs
@@ -161,7 +161,7 @@ impl fmt::Display for Throughput {
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use sp_runtime::assert_eq_error_rate;
+	use sp_runtime::assert_eq_error_rate_float;
 
 	/// `SUBSTRATE_REFERENCE_HARDWARE` can be en- and decoded.
 	#[test]
@@ -179,9 +179,9 @@ mod tests {
 		const EPS: f64 = 0.1;
 		let gib = Throughput::GiBs(14.324);
 
-		assert_eq_error_rate!(14.324, gib.to_gibs(), EPS);
-		assert_eq_error_rate!(14667.776, gib.to_mibs(), EPS);
-		assert_eq_error_rate!(14667.776 * 1024.0, gib.to_kibs(), EPS);
+		assert_eq_error_rate_float!(14.324, gib.to_gibs(), EPS);
+		assert_eq_error_rate_float!(14667.776, gib.to_mibs(), EPS);
+		assert_eq_error_rate_float!(14667.776 * 1024.0, gib.to_kibs(), EPS);
 		assert_eq!("14.32 GiB/s", gib.to_string());
 		assert_eq!("14.32 GiB/s", gib.normalize().to_string());
 
-- 
GitLab