diff --git a/Cargo.lock b/Cargo.lock index ae4a6b851..03ab45c9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1924,7 +1924,7 @@ dependencies = [ [[package]] name = "da-runtime" -version = "7.0.0" +version = "7.0.1" dependencies = [ "avail-core", "codspeed-criterion-compat", @@ -1969,6 +1969,7 @@ dependencies = [ "pallet-mmr", "pallet-multisig", "pallet-nomination-pools", + "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-preimage", "pallet-scheduler", @@ -6436,7 +6437,6 @@ dependencies = [ [[package]] name = "pallet-nomination-pools" version = "1.0.0" -source = "git+https://github.com/paritytech/substrate.git/?branch=polkadot-v1.0.0#948fbd2fd1233dc26dbb9f9bbc1d2cca2c03945d" dependencies = [ "frame-support", "frame-system", @@ -6450,6 +6450,16 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-nomination-pools-runtime-api" +version = "1.0.0-dev" +dependencies = [ + "pallet-nomination-pools", + "parity-scale-codec", + "sp-api", + "sp-std", +] + [[package]] name = "pallet-offences" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index e56c7ca19..ac9749e59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,8 @@ pallet-mmr = { git = "https://github.com/paritytech/substrate.git", branch = "po pallet-multisig = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } pallet-child-bounties = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } pallet-preimage = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } -pallet-nomination-pools = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } +pallet-nomination-pools = { path = "pallets/nomination-pools" } +pallet-nomination-pools-runtime-api = { path = "pallets/nomination-pools/runtime-api" } pallet-election-provider-support-benchmarking = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } substrate-wasm-builder = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } pallet-identity = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" } diff --git a/pallets/nomination-pools/Cargo.toml b/pallets/nomination-pools/Cargo.toml new file mode 100644 index 000000000..54b59f568 --- /dev/null +++ b/pallets/nomination-pools/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-nomination-pools" +version = "1.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME nomination pools pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# parity +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } + +# FRAME +frame-support = { version = "4.0.0-dev", default-features = false } +frame-system = { version = "4.0.0-dev", default-features = false } +sp-runtime = { version = "24.0.0", default-features = false } +sp-std = { version = "8.0.0", default-features = false } +sp-staking = { version = "4.0.0-dev", default-features = false } +sp-core = { version = "21.0.0", default-features = false } +sp-io = { version = "23.0.0", default-features = false } +log = { version = "0.4.0", default-features = false } + +# Optional: use for testing and/or fuzzing +pallet-balances = { version = "4.0.0-dev", optional = true } +sp-tracing = { version = "10.0.0", optional = true } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev" } +sp-tracing = { version = "10.0.0" } + +[features] +default = ["std"] +fuzzing = ["pallet-balances", "sp-tracing"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-std/std", + "sp-io/std", + "sp-staking/std", + "sp-core/std", + "log/std", +] +runtime-benchmarks = [ + "sp-staking/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime" +] diff --git a/pallets/nomination-pools/benchmarking/Cargo.toml b/pallets/nomination-pools/benchmarking/Cargo.toml new file mode 100644 index 000000000..ea96dc4e2 --- /dev/null +++ b/pallets/nomination-pools/benchmarking/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "pallet-nomination-pools-benchmarking" +version = "1.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME nomination pools pallet benchmarking" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# parity +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } + +# FRAME +frame-benchmarking = { version = "4.0.0-dev", default-features = false } +frame-election-provider-support = { version = "4.0.0-dev", default-features = false } +frame-support = { version = "4.0.0-dev", default-features = false } +frame-system = { version = "4.0.0-dev", default-features = false } +pallet-bags-list = { version = "4.0.0-dev", default-features = false } +pallet-staking = { version = "4.0.0-dev", default-features = false } +pallet-nomination-pools = { version = "1.0.0", default-features = false, path = "../" } + +# Substrate Primitives +sp-runtime = { version = "24.0.0", default-features = false } +sp-runtime-interface = { version = "17.0.0", default-features = false } +sp-staking = { version = "4.0.0-dev", default-features = false } +sp-std = { version = "8.0.0", default-features = false } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev", default-features = false } +pallet-timestamp = { version = "4.0.0-dev" } +pallet-staking-reward-curve = { version = "4.0.0-dev" } +sp-core = { version = "21.0.0" } +sp-io = { version = "23.0.0" } + +[features] +default = ["std"] + +std = [ + "frame-benchmarking/std", + "frame-election-provider-support/std", + "frame-support/std", + "frame-system/std", + "pallet-bags-list/std", + "pallet-staking/std", + "pallet-nomination-pools/std", + "sp-runtime/std", + "sp-runtime-interface/std", + "sp-staking/std", + "sp-std/std", +] + +runtime-benchmarks = [ + "frame-election-provider-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-staking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "pallet-staking/runtime-benchmarks", + "pallet-nomination-pools/runtime-benchmarks", + "pallet-bags-list/runtime-benchmarks", +] diff --git a/pallets/nomination-pools/benchmarking/README.md b/pallets/nomination-pools/benchmarking/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/pallets/nomination-pools/benchmarking/src/lib.rs b/pallets/nomination-pools/benchmarking/src/lib.rs new file mode 100644 index 000000000..30bef6221 --- /dev/null +++ b/pallets/nomination-pools/benchmarking/src/lib.rs @@ -0,0 +1,805 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Benchmarks for the nomination pools coupled with the staking and bags list pallets. + +#![cfg(feature = "runtime-benchmarks")] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; + +use frame_benchmarking::v1::{ + account, frame_support::traits::Currency, vec, whitelist_account, Vec, +}; +use frame_election_provider_support::SortedListProvider; +use frame_support::{assert_ok, ensure, traits::Get}; +use frame_system::RawOrigin as RuntimeOrigin; +use pallet_nomination_pools::{ + BalanceOf, BondExtra, BondedPoolInner, BondedPools, ClaimPermission, ClaimPermissions, + Commission, CommissionChangeRate, ConfigOp, GlobalMaxCommission, MaxPoolMembers, + MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond, Pallet as Pools, + PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage, +}; +use sp_runtime::{ + traits::{Bounded, StaticLookup, Zero}, + Perbill, +}; +use sp_staking::{EraIndex, StakingInterface}; +// `frame_benchmarking::benchmarks!` macro needs this +use pallet_nomination_pools::Call; + +type CurrencyOf = ::Currency; + +const USER_SEED: u32 = 0; +const MAX_SPANS: u32 = 100; + +type VoterBagsListInstance = pallet_bags_list::Instance1; +pub trait Config: + pallet_nomination_pools::Config + + pallet_staking::Config + + pallet_bags_list::Config +{ +} + +pub struct Pallet(Pools); + +fn create_funded_user_with_balance( + string: &'static str, + n: u32, + balance: BalanceOf, +) -> T::AccountId { + let user = account(string, n, USER_SEED); + T::Currency::make_free_balance_be(&user, balance); + user +} + +// Create a bonded pool account, bonding `balance` and giving the account `balance * 2` free +// balance. +fn create_pool_account( + n: u32, + balance: BalanceOf, + commission: Option, +) -> (T::AccountId, T::AccountId) { + let ed = CurrencyOf::::minimum_balance(); + let pool_creator: T::AccountId = + create_funded_user_with_balance::("pool_creator", n, ed + balance * 2u32.into()); + let pool_creator_lookup = T::Lookup::unlookup(pool_creator.clone()); + + Pools::::create( + RuntimeOrigin::Signed(pool_creator.clone()).into(), + balance, + pool_creator_lookup.clone(), + pool_creator_lookup.clone(), + pool_creator_lookup, + ) + .unwrap(); + + if let Some(c) = commission { + let pool_id = pallet_nomination_pools::LastPoolId::::get(); + Pools::::set_commission( + RuntimeOrigin::Signed(pool_creator.clone()).into(), + pool_id, + Some((c, pool_creator.clone())), + ) + .expect("pool just created, commission can be set by root; qed"); + } + + let pool_account = pallet_nomination_pools::BondedPools::::iter() + .find(|(_, bonded_pool)| bonded_pool.roles.depositor == pool_creator) + .map(|(pool_id, _)| Pools::::create_bonded_account(pool_id)) + .expect("pool_creator created a pool above"); + + (pool_creator, pool_account) +} + +fn vote_to_balance( + vote: u64, +) -> Result, &'static str> { + vote.try_into().map_err(|_| "could not convert u64 to Balance") +} + +#[allow(unused)] +struct ListScenario { + /// Stash/Controller that is expected to be moved. + origin1: T::AccountId, + creator1: T::AccountId, + dest_weight: BalanceOf, + origin1_member: Option, +} + +impl ListScenario { + /// An expensive scenario for bags-list implementation: + /// + /// - the node to be updated (r) is the head of a bag that has at least one other node. The bag + /// itself will need to be read and written to update its head. The node pointed to by r.next + /// will need to be read and written as it will need to have its prev pointer updated. Note + /// that there are two other worst case scenarios for bag removal: 1) the node is a tail and + /// 2) the node is a middle node with prev and next; all scenarios end up with the same number + /// of storage reads and writes. + /// + /// - the destination bag has at least one node, which will need its next pointer updated. + pub(crate) fn new( + origin_weight: BalanceOf, + is_increase: bool, + ) -> Result { + ensure!(!origin_weight.is_zero(), "origin weight must be greater than 0"); + + ensure!( + pallet_nomination_pools::MaxPools::::get().unwrap_or(0) >= 3, + "must allow at least three pools for benchmarks" + ); + + // Burn the entire issuance. + let i = CurrencyOf::::burn(CurrencyOf::::total_issuance()); + sp_std::mem::forget(i); + + // Create accounts with the origin weight + let (pool_creator1, pool_origin1) = + create_pool_account::(USER_SEED + 1, origin_weight, Some(Perbill::from_percent(50))); + + T::Staking::nominate( + &pool_origin1, + // NOTE: these don't really need to be validators. + vec![account("random_validator", 0, USER_SEED)], + )?; + + let (_, pool_origin2) = + create_pool_account::(USER_SEED + 2, origin_weight, Some(Perbill::from_percent(50))); + + T::Staking::nominate( + &pool_origin2, + vec![account("random_validator", 0, USER_SEED)].clone(), + )?; + + // Find a destination weight that will trigger the worst case scenario + let dest_weight_as_vote = ::VoterList::score_update_worst_case( + &pool_origin1, + is_increase, + ); + + let dest_weight: BalanceOf = + dest_weight_as_vote.try_into().map_err(|_| "could not convert u64 to Balance")?; + + // Create an account with the worst case destination weight + let (_, pool_dest1) = + create_pool_account::(USER_SEED + 3, dest_weight, Some(Perbill::from_percent(50))); + + T::Staking::nominate(&pool_dest1, vec![account("random_validator", 0, USER_SEED)])?; + + let weight_of = pallet_staking::Pallet::::weight_of_fn(); + assert_eq!(vote_to_balance::(weight_of(&pool_origin1)).unwrap(), origin_weight); + assert_eq!(vote_to_balance::(weight_of(&pool_origin2)).unwrap(), origin_weight); + assert_eq!(vote_to_balance::(weight_of(&pool_dest1)).unwrap(), dest_weight); + + Ok(ListScenario { + origin1: pool_origin1, + creator1: pool_creator1, + dest_weight, + origin1_member: None, + }) + } + + fn add_joiner(mut self, amount: BalanceOf) -> Self { + let amount = MinJoinBond::::get() + .max(CurrencyOf::::minimum_balance()) + // Max `amount` with minimum thresholds for account balance and joining a pool + // to ensure 1) the user can be created and 2) can join the pool + .max(amount); + + let joiner: T::AccountId = account("joiner", USER_SEED, 0); + self.origin1_member = Some(joiner.clone()); + CurrencyOf::::make_free_balance_be(&joiner, amount * 2u32.into()); + + let original_bonded = T::Staking::active_stake(&self.origin1).unwrap(); + + // Unbond `amount` from the underlying pool account so when the member joins + // we will maintain `current_bonded`. + T::Staking::unbond(&self.origin1, amount).expect("the pool was created in `Self::new`."); + + // Account pool points for the unbonded balance. + BondedPools::::mutate(&1, |maybe_pool| { + maybe_pool.as_mut().map(|pool| pool.points -= amount) + }); + + Pools::::join(RuntimeOrigin::Signed(joiner.clone()).into(), amount, 1).unwrap(); + + // check that the vote weight is still the same as the original bonded + let weight_of = pallet_staking::Pallet::::weight_of_fn(); + assert_eq!(vote_to_balance::(weight_of(&self.origin1)).unwrap(), original_bonded); + + // check the member was added correctly + let member = PoolMembers::::get(&joiner).unwrap(); + assert_eq!(member.points, amount); + assert_eq!(member.pool_id, 1); + + self + } +} + +frame_benchmarking::benchmarks! { + join { + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); + + // setup the worst case list scenario. + let scenario = ListScenario::::new(origin_weight, true)?; + assert_eq!( + T::Staking::active_stake(&scenario.origin1).unwrap(), + origin_weight + ); + + let max_additional = scenario.dest_weight - origin_weight; + let joiner_free = CurrencyOf::::minimum_balance() + max_additional; + + let joiner: T::AccountId + = create_funded_user_with_balance::("joiner", 0, joiner_free); + + whitelist_account!(joiner); + }: _(RuntimeOrigin::Signed(joiner.clone()), max_additional, 1) + verify { + assert_eq!(CurrencyOf::::free_balance(&joiner), joiner_free - max_additional); + assert_eq!( + T::Staking::active_stake(&scenario.origin1).unwrap(), + scenario.dest_weight + ); + } + + bond_extra_transfer { + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); + let scenario = ListScenario::::new(origin_weight, true)?; + let extra = scenario.dest_weight - origin_weight; + + // creator of the src pool will bond-extra, bumping itself to dest bag. + + }: bond_extra(RuntimeOrigin::Signed(scenario.creator1.clone()), BondExtra::FreeBalance(extra)) + verify { + assert!( + T::Staking::active_stake(&scenario.origin1).unwrap() >= + scenario.dest_weight + ); + } + + bond_extra_other { + let claimer: T::AccountId = account("claimer", USER_SEED + 4, 0); + + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); + let scenario = ListScenario::::new(origin_weight, true)?; + let extra = (scenario.dest_weight - origin_weight).max(CurrencyOf::::minimum_balance()); + + // set claim preferences to `PermissionlessAll` to any account to bond extra on member's behalf. + let _ = Pools::::set_claim_permission(RuntimeOrigin::Signed(scenario.creator1.clone()).into(), ClaimPermission::PermissionlessAll); + + // transfer exactly `extra` to the depositor of the src pool (1), + let reward_account1 = Pools::::create_reward_account(1); + assert!(extra >= CurrencyOf::::minimum_balance()); + CurrencyOf::::deposit_creating(&reward_account1, extra); + + }: _(RuntimeOrigin::Signed(claimer), T::Lookup::unlookup(scenario.creator1.clone()), BondExtra::Rewards) + verify { + // commission of 50% deducted here. + assert!( + T::Staking::active_stake(&scenario.origin1).unwrap() >= + scenario.dest_weight / 2u32.into() + ); + } + + claim_payout { + let claimer: T::AccountId = account("claimer", USER_SEED + 4, 0); + let commission = Perbill::from_percent(50); + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); + let ed = CurrencyOf::::minimum_balance(); + let (depositor, pool_account) = create_pool_account::(0, origin_weight, Some(commission)); + let reward_account = Pools::::create_reward_account(1); + + // Send funds to the reward account of the pool + CurrencyOf::::make_free_balance_be(&reward_account, ed + origin_weight); + + // set claim preferences to `PermissionlessAll` so any account can claim rewards on member's + // behalf. + let _ = Pools::::set_claim_permission(RuntimeOrigin::Signed(depositor.clone()).into(), ClaimPermission::PermissionlessAll); + + // Sanity check + assert_eq!( + CurrencyOf::::free_balance(&depositor), + origin_weight + ); + + whitelist_account!(depositor); + }:claim_payout_other(RuntimeOrigin::Signed(claimer), depositor.clone()) + verify { + assert_eq!( + CurrencyOf::::free_balance(&depositor), + origin_weight + commission * origin_weight + ); + assert_eq!( + CurrencyOf::::free_balance(&reward_account), + ed + commission * origin_weight + ); + } + + + 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 = Pools::::depositor_min_bond() * 200u32.into(); + let scenario = ListScenario::::new(origin_weight, false)?; + let amount = origin_weight - scenario.dest_weight; + + let scenario = scenario.add_joiner(amount); + let member_id = scenario.origin1_member.unwrap().clone(); + let member_id_lookup = T::Lookup::unlookup(member_id.clone()); + let all_points = PoolMembers::::get(&member_id).unwrap().points; + whitelist_account!(member_id); + }: _(RuntimeOrigin::Signed(member_id.clone()), member_id_lookup, all_points) + verify { + let bonded_after = T::Staking::active_stake(&scenario.origin1).unwrap(); + // We at least went down to the destination bag + assert!(bonded_after <= scenario.dest_weight); + let member = PoolMembers::::get( + &member_id + ) + .unwrap(); + assert_eq!( + member.unbonding_eras.keys().cloned().collect::>(), + vec![0 + T::Staking::bonding_duration()] + ); + assert_eq!( + member.unbonding_eras.values().cloned().collect::>(), + vec![all_points] + ); + } + + pool_withdraw_unbonded { + let s in 0 .. MAX_SPANS; + + let min_create_bond = Pools::::depositor_min_bond(); + let (depositor, pool_account) = create_pool_account::(0, min_create_bond, None); + + // Add a new member + let min_join_bond = MinJoinBond::::get().max(CurrencyOf::::minimum_balance()); + let joiner = create_funded_user_with_balance::("joiner", 0, min_join_bond * 2u32.into()); + Pools::::join(RuntimeOrigin::Signed(joiner.clone()).into(), min_join_bond, 1) + .unwrap(); + + // Sanity check join worked + assert_eq!( + T::Staking::active_stake(&pool_account).unwrap(), + min_create_bond + min_join_bond + ); + assert_eq!(CurrencyOf::::free_balance(&joiner), min_join_bond); + + // Unbond the new member + Pools::::fully_unbond(RuntimeOrigin::Signed(joiner.clone()).into(), joiner.clone()).unwrap(); + + // Sanity check that unbond worked + assert_eq!( + T::Staking::active_stake(&pool_account).unwrap(), + min_create_bond + ); + assert_eq!(pallet_staking::Ledger::::get(&pool_account).unwrap().unlocking.len(), 1); + // Set the current era + pallet_staking::CurrentEra::::put(EraIndex::max_value()); + + // Add `s` count of slashing spans to storage. + pallet_staking::benchmarking::add_slashing_spans::(&pool_account, s); + whitelist_account!(pool_account); + }: _(RuntimeOrigin::Signed(pool_account.clone()), 1, s) + verify { + // The joiners funds didn't change + assert_eq!(CurrencyOf::::free_balance(&joiner), min_join_bond); + // The unlocking chunk was removed + assert_eq!(pallet_staking::Ledger::::get(pool_account).unwrap().unlocking.len(), 0); + } + + withdraw_unbonded_update { + let s in 0 .. MAX_SPANS; + + let min_create_bond = Pools::::depositor_min_bond(); + let (depositor, pool_account) = create_pool_account::(0, min_create_bond, None); + + // Add a new member + let min_join_bond = MinJoinBond::::get().max(CurrencyOf::::minimum_balance()); + let joiner = create_funded_user_with_balance::("joiner", 0, min_join_bond * 2u32.into()); + let joiner_lookup = T::Lookup::unlookup(joiner.clone()); + Pools::::join(RuntimeOrigin::Signed(joiner.clone()).into(), min_join_bond, 1) + .unwrap(); + + // Sanity check join worked + assert_eq!( + T::Staking::active_stake(&pool_account).unwrap(), + min_create_bond + min_join_bond + ); + assert_eq!(CurrencyOf::::free_balance(&joiner), min_join_bond); + + // Unbond the new member + pallet_staking::CurrentEra::::put(0); + Pools::::fully_unbond(RuntimeOrigin::Signed(joiner.clone()).into(), joiner.clone()).unwrap(); + + // Sanity check that unbond worked + assert_eq!( + T::Staking::active_stake(&pool_account).unwrap(), + min_create_bond + ); + assert_eq!(pallet_staking::Ledger::::get(&pool_account).unwrap().unlocking.len(), 1); + + // Set the current era to ensure we can withdraw unbonded funds + pallet_staking::CurrentEra::::put(EraIndex::max_value()); + + pallet_staking::benchmarking::add_slashing_spans::(&pool_account, s); + whitelist_account!(joiner); + }: withdraw_unbonded(RuntimeOrigin::Signed(joiner.clone()), joiner_lookup, s) + verify { + assert_eq!( + CurrencyOf::::free_balance(&joiner), + min_join_bond * 2u32.into() + ); + // The unlocking chunk was removed + assert_eq!(pallet_staking::Ledger::::get(&pool_account).unwrap().unlocking.len(), 0); + } + + withdraw_unbonded_kill { + let s in 0 .. MAX_SPANS; + + let min_create_bond = Pools::::depositor_min_bond(); + let (depositor, pool_account) = create_pool_account::(0, min_create_bond, None); + let depositor_lookup = T::Lookup::unlookup(depositor.clone()); + + // We set the pool to the destroying state so the depositor can leave + BondedPools::::try_mutate(&1, |maybe_bonded_pool| { + maybe_bonded_pool.as_mut().ok_or(()).map(|bonded_pool| { + bonded_pool.state = PoolState::Destroying; + }) + }) + .unwrap(); + + // Unbond the creator + pallet_staking::CurrentEra::::put(0); + // Simulate some rewards so we can check if the rewards storage is cleaned up. We check this + // here to ensure the complete flow for destroying a pool works - the reward pool account + // should never exist by time the depositor withdraws so we test that it gets cleaned + // up when unbonding. + let reward_account = Pools::::create_reward_account(1); + assert!(frame_system::Account::::contains_key(&reward_account)); + Pools::::fully_unbond(RuntimeOrigin::Signed(depositor.clone()).into(), depositor.clone()).unwrap(); + + // Sanity check that unbond worked + assert_eq!( + T::Staking::active_stake(&pool_account).unwrap(), + Zero::zero() + ); + assert_eq!( + CurrencyOf::::free_balance(&pool_account), + min_create_bond + ); + assert_eq!(pallet_staking::Ledger::::get(&pool_account).unwrap().unlocking.len(), 1); + + // Set the current era to ensure we can withdraw unbonded funds + pallet_staking::CurrentEra::::put(EraIndex::max_value()); + + // Some last checks that storage items we expect to get cleaned up are present + assert!(pallet_staking::Ledger::::contains_key(&pool_account)); + assert!(BondedPools::::contains_key(&1)); + assert!(SubPoolsStorage::::contains_key(&1)); + assert!(RewardPools::::contains_key(&1)); + assert!(PoolMembers::::contains_key(&depositor)); + assert!(frame_system::Account::::contains_key(&reward_account)); + + whitelist_account!(depositor); + }: withdraw_unbonded(RuntimeOrigin::Signed(depositor.clone()), depositor_lookup, s) + verify { + // Pool removal worked + assert!(!pallet_staking::Ledger::::contains_key(&pool_account)); + assert!(!BondedPools::::contains_key(&1)); + assert!(!SubPoolsStorage::::contains_key(&1)); + assert!(!RewardPools::::contains_key(&1)); + assert!(!PoolMembers::::contains_key(&depositor)); + assert!(!frame_system::Account::::contains_key(&pool_account)); + assert!(!frame_system::Account::::contains_key(&reward_account)); + + // Funds where transferred back correctly + assert_eq!( + CurrencyOf::::free_balance(&depositor), + // gets bond back + rewards collecting when unbonding + min_create_bond * 2u32.into() + CurrencyOf::::minimum_balance() + ); + } + + create { + let min_create_bond = Pools::::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::::make_free_balance_be(&depositor, min_create_bond * 2u32.into()); + + // Make sure no Pools exist at a pre-condition for our verify checks + assert_eq!(RewardPools::::count(), 0); + assert_eq!(BondedPools::::count(), 0); + + whitelist_account!(depositor); + }: _( + RuntimeOrigin::Signed(depositor.clone()), + min_create_bond, + depositor_lookup.clone(), + depositor_lookup.clone(), + depositor_lookup + ) + verify { + assert_eq!(RewardPools::::count(), 1); + assert_eq!(BondedPools::::count(), 1); + let (_, new_pool) = BondedPools::::iter().next().unwrap(); + assert_eq!( + new_pool, + BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: min_create_bond, + roles: PoolRoles { + depositor: depositor.clone(), + root: Some(depositor.clone()), + nominator: Some(depositor.clone()), + bouncer: Some(depositor.clone()), + }, + state: PoolState::Open, + } + ); + assert_eq!( + T::Staking::active_stake(&Pools::::create_bonded_account(1)), + Ok(min_create_bond) + ); + } + + nominate { + let n in 1 .. T::MaxNominations::get(); + + // Create a pool + let min_create_bond = Pools::::depositor_min_bond() * 2u32.into(); + let (depositor, pool_account) = create_pool_account::(0, min_create_bond, None); + + // Create some accounts to nominate. For the sake of benchmarking they don't need to be + // actual validators + let validators: Vec<_> = (0..n) + .map(|i| account("stash", USER_SEED, i)) + .collect(); + + whitelist_account!(depositor); + }:_(RuntimeOrigin::Signed(depositor.clone()), 1, validators) + verify { + assert_eq!(RewardPools::::count(), 1); + assert_eq!(BondedPools::::count(), 1); + let (_, new_pool) = BondedPools::::iter().next().unwrap(); + assert_eq!( + new_pool, + BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: min_create_bond, + roles: PoolRoles { + depositor: depositor.clone(), + root: Some(depositor.clone()), + nominator: Some(depositor.clone()), + bouncer: Some(depositor.clone()), + }, + state: PoolState::Open, + } + ); + assert_eq!( + T::Staking::active_stake(&Pools::::create_bonded_account(1)), + Ok(min_create_bond) + ); + } + + set_state { + // Create a pool + let min_create_bond = Pools::::depositor_min_bond(); + let (depositor, pool_account) = create_pool_account::(0, min_create_bond, None); + BondedPools::::mutate(&1, |maybe_pool| { + // Force the pool into an invalid state + maybe_pool.as_mut().map(|pool| pool.points = min_create_bond * 10u32.into()); + }); + + let caller = account("caller", 0, USER_SEED); + whitelist_account!(caller); + }:_(RuntimeOrigin::Signed(caller), 1, PoolState::Destroying) + verify { + assert_eq!(BondedPools::::get(1).unwrap().state, PoolState::Destroying); + } + + set_metadata { + let n in 1 .. ::MaxMetadataLen::get(); + + // Create a pool + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), None); + + // Create metadata of the max possible size + let metadata: Vec = (0..n).map(|_| 42).collect(); + + whitelist_account!(depositor); + }:_(RuntimeOrigin::Signed(depositor), 1, metadata.clone()) + verify { + assert_eq!(Metadata::::get(&1), metadata); + } + + set_configs { + }:_( + RuntimeOrigin::Root, + ConfigOp::Set(BalanceOf::::max_value()), + ConfigOp::Set(BalanceOf::::max_value()), + ConfigOp::Set(u32::MAX), + ConfigOp::Set(u32::MAX), + ConfigOp::Set(u32::MAX), + ConfigOp::Set(Perbill::max_value()) + ) verify { + assert_eq!(MinJoinBond::::get(), BalanceOf::::max_value()); + assert_eq!(MinCreateBond::::get(), BalanceOf::::max_value()); + assert_eq!(MaxPools::::get(), Some(u32::MAX)); + assert_eq!(MaxPoolMembers::::get(), Some(u32::MAX)); + assert_eq!(MaxPoolMembersPerPool::::get(), Some(u32::MAX)); + assert_eq!(GlobalMaxCommission::::get(), Some(Perbill::max_value())); + } + + update_roles { + let first_id = pallet_nomination_pools::LastPoolId::::get() + 1; + let (root, _) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), None); + let random: T::AccountId = account("but is anything really random in computers..?", 0, USER_SEED); + }:_( + RuntimeOrigin::Signed(root.clone()), + first_id, + ConfigOp::Set(random.clone()), + ConfigOp::Set(random.clone()), + ConfigOp::Set(random.clone()) + ) verify { + assert_eq!( + pallet_nomination_pools::BondedPools::::get(first_id).unwrap().roles, + pallet_nomination_pools::PoolRoles { + depositor: root, + nominator: Some(random.clone()), + bouncer: Some(random.clone()), + root: Some(random), + }, + ) + } + + chill { + // Create a pool + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), None); + + // Nominate with the pool. + let validators: Vec<_> = (0..T::MaxNominations::get()) + .map(|i| account("stash", USER_SEED, i)) + .collect(); + + assert_ok!(T::Staking::nominate(&pool_account, validators)); + assert!(T::Staking::nominations(&Pools::::create_bonded_account(1)).is_some()); + + whitelist_account!(depositor); + }:_(RuntimeOrigin::Signed(depositor.clone()), 1) + verify { + assert!(T::Staking::nominations(&Pools::::create_bonded_account(1)).is_none()); + } + + set_commission { + // Create a pool - do not set a commission yet. + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), None); + // set a max commission + Pools::::set_commission_max(RuntimeOrigin::Signed(depositor.clone()).into(), 1u32.into(), Perbill::from_percent(50)).unwrap(); + // set a change rate + Pools::::set_commission_change_rate(RuntimeOrigin::Signed(depositor.clone()).into(), 1u32.into(), CommissionChangeRate { + max_increase: Perbill::from_percent(20), + min_delay: 0u32.into(), + }).unwrap(); + + }:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Some((Perbill::from_percent(20), depositor.clone()))) + verify { + assert_eq!(BondedPools::::get(1).unwrap().commission, Commission { + current: Some((Perbill::from_percent(20), depositor)), + max: Some(Perbill::from_percent(50)), + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(20), + min_delay: 0u32.into() + }), + throttle_from: Some(1u32.into()), + }); + } + + set_commission_max { + // Create a pool, setting a commission that will update when max commission is set. + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), Some(Perbill::from_percent(50))); + }:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Perbill::from_percent(50)) + verify { + assert_eq!( + BondedPools::::get(1).unwrap().commission, Commission { + current: Some((Perbill::from_percent(50), depositor)), + max: Some(Perbill::from_percent(50)), + change_rate: None, + throttle_from: Some(0u32.into()), + }); + } + + set_commission_change_rate { + // Create a pool + let (depositor, pool_account) = create_pool_account::(0, Pools::::depositor_min_bond() * 2u32.into(), None); + }:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), CommissionChangeRate { + max_increase: Perbill::from_percent(50), + min_delay: 1000u32.into(), + }) + verify { + assert_eq!( + BondedPools::::get(1).unwrap().commission, Commission { + current: None, + max: None, + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(50), + min_delay: 1000u32.into(), + }), + throttle_from: Some(1_u32.into()), + }); + } + + set_claim_permission { + // Create a pool + let min_create_bond = Pools::::depositor_min_bond(); + let (depositor, pool_account) = create_pool_account::(0, min_create_bond, None); + + // Join pool + let min_join_bond = MinJoinBond::::get().max(CurrencyOf::::minimum_balance()); + let joiner = create_funded_user_with_balance::("joiner", 0, min_join_bond * 4u32.into()); + let joiner_lookup = T::Lookup::unlookup(joiner.clone()); + Pools::::join(RuntimeOrigin::Signed(joiner.clone()).into(), min_join_bond, 1) + .unwrap(); + + // Sanity check join worked + assert_eq!( + T::Staking::active_stake(&pool_account).unwrap(), + min_create_bond + min_join_bond + ); + }:_(RuntimeOrigin::Signed(joiner.clone()), ClaimPermission::PermissionlessAll) + verify { + assert_eq!(ClaimPermissions::::get(joiner), ClaimPermission::PermissionlessAll); + } + + claim_commission { + let claimer: T::AccountId = account("claimer_member", USER_SEED + 4, 0); + let commission = Perbill::from_percent(50); + let origin_weight = Pools::::depositor_min_bond() * 2u32.into(); + let ed = CurrencyOf::::minimum_balance(); + let (depositor, pool_account) = create_pool_account::(0, origin_weight, Some(commission)); + let reward_account = Pools::::create_reward_account(1); + CurrencyOf::::make_free_balance_be(&reward_account, ed + origin_weight); + + // member claims a payout to make some commission available. + let _ = Pools::::claim_payout(RuntimeOrigin::Signed(claimer).into()); + + whitelist_account!(depositor); + }:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into()) + verify { + assert_eq!( + CurrencyOf::::free_balance(&depositor), + origin_weight + commission * origin_weight + ); + assert_eq!( + CurrencyOf::::free_balance(&reward_account), + ed + commission * origin_weight + ); + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::new_test_ext(), + crate::mock::Runtime + ); +} diff --git a/pallets/nomination-pools/benchmarking/src/mock.rs b/pallets/nomination-pools/benchmarking/src/mock.rs new file mode 100644 index 000000000..2298f611d --- /dev/null +++ b/pallets/nomination-pools/benchmarking/src/mock.rs @@ -0,0 +1,202 @@ +// This file is part of Substrate. + +// Copyright (C) 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::VoterBagsListInstance; +use frame_election_provider_support::VoteWeight; +use frame_support::{pallet_prelude::*, parameter_types, traits::ConstU64, PalletId}; +use sp_runtime::{ + traits::{Convert, IdentityLookup}, + BuildStorage, FixedU128, Perbill, +}; + +type AccountId = u128; +type Nonce = u32; +type BlockNumber = u64; +type Balance = u128; + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = Nonce; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + 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 const ExistentialDeposit: Balance = 10; +} +impl pallet_balances::Config for Runtime { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type MaxHolds = (); +} + +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; +} +impl pallet_staking::Config for Runtime { + type MaxNominations = ConstU32<16>; + type Currency = Balances; + type CurrencyBalance = Balance; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type AdminOrigin = frame_system::EnsureRoot; + type BondingDuration = ConstU32<3>; + type SessionInterface = (); + type EraPayout = pallet_staking::ConvertCurve; + type NextNewSession = (); + type MaxNominatorRewardedPerValidator = ConstU32<64>; + type OffendingValidatorsThreshold = (); + type ElectionProvider = + frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = VoterList; + type TargetList = pallet_staking::UseValidatorsMap; + type MaxUnlockingChunks = ConstU32<32>; + type HistoryDepth = ConstU32<84>; + type EventListeners = Pools; + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type WeightInfo = (); +} + +parameter_types! { + pub static BagThresholds: &'static [VoteWeight] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; +} + +impl pallet_bags_list::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type BagThresholds = BagThresholds; + type ScoreProvider = Staking; + type Score = VoteWeight; +} + +pub struct BalanceToU256; +impl Convert for BalanceToU256 { + fn convert(n: Balance) -> sp_core::U256 { + n.into() + } +} + +pub struct U256ToBalance; +impl Convert for U256ToBalance { + fn convert(n: sp_core::U256) -> Balance { + n.try_into().unwrap() + } +} + +parameter_types! { + pub static PostUnbondingPoolsWindow: u32 = 10; + pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls"); + pub const MaxPointsToBalance: u8 = 10; +} + +impl pallet_nomination_pools::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RewardCounter = FixedU128; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = Staking; + type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow; + type MaxMetadataLen = ConstU32<256>; + type MaxUnbonding = ConstU32<8>; + type PalletId = PoolsPalletId; + type MaxPointsToBalance = MaxPointsToBalance; +} + +impl crate::Config for Runtime {} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub struct Runtime + { + System: frame_system::{Pallet, Call, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Staking: pallet_staking::{Pallet, Call, Config, Storage, Event}, + VoterList: pallet_bags_list::::{Pallet, Call, Storage, Event}, + Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event}, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let _ = pallet_nomination_pools::GenesisConfig:: { + min_join_bond: 2, + min_create_bond: 2, + max_pools: Some(3), + max_members_per_pool: Some(3), + max_members: Some(3 * 3), + global_max_commission: Some(Perbill::from_percent(50)), + } + .assimilate_storage(&mut storage); + sp_io::TestExternalities::from(storage) +} diff --git a/pallets/nomination-pools/fuzzer/Cargo.toml b/pallets/nomination-pools/fuzzer/Cargo.toml new file mode 100644 index 000000000..9ef5e9449 --- /dev/null +++ b/pallets/nomination-pools/fuzzer/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pallet-nomination-pools-fuzzer" +version = "2.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Fuzzer for fixed point arithmetic primitives." +documentation = "https://docs.rs/sp-arithmetic-fuzzer" +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +honggfuzz = "0.5.54" + +pallet-nomination-pools = { path = "..", features = ["fuzzing"] } + +frame-system = { version = "4.0.0-dev" } +frame-support = { version = "4.0.0-dev" } + +sp-runtime = { version = "24.0.0" } +sp-io = { version = "23.0.0" } +sp-tracing = { version = "10.0.0" } + +rand = { version = "0.8.5", features = ["small_rng"] } +log = "0.4.17" + +[[bin]] +name = "call" +path = "src/call.rs" diff --git a/pallets/nomination-pools/fuzzer/src/call.rs b/pallets/nomination-pools/fuzzer/src/call.rs new file mode 100644 index 000000000..027fb2b69 --- /dev/null +++ b/pallets/nomination-pools/fuzzer/src/call.rs @@ -0,0 +1,354 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! # Running +//! Running this fuzzer can be done with `cargo hfuzz run call`. `honggfuzz` CLI +//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads. +//! +//! # Debugging a panic +//! Once a panic is found, it can be debugged with +//! `cargo hfuzz run-debug per_thing_rational hfuzz_workspace/call/*.fuzz`. + +use frame_support::{ + assert_ok, + traits::{Currency, GetCallName, UnfilteredDispatchable}, +}; +use honggfuzz::fuzz; +use pallet_nomination_pools::{ + log, + mock::*, + pallet as pools, + pallet::{BondedPools, Call as PoolsCall, Event as PoolsEvents, PoolMembers}, + BondExtra, BondedPool, GlobalMaxCommission, LastPoolId, MaxPoolMembers, MaxPoolMembersPerPool, + MaxPools, MinCreateBond, MinJoinBond, PoolId, +}; +use rand::{seq::SliceRandom, Rng}; +use sp_runtime::{assert_eq_error_rate, Perbill, 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(rng: &mut R) -> (RuntimeOrigin, AccountId) { + let count = PoolMembers::::count(); + if rng.gen::() && 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::::iter_keys().skip(skip).take(1).next().unwrap(); + let acc = + if candidate == REWARD_AGENT_ACCOUNT { rng.gen::() } else { candidate }; + + (RuntimeOrigin::signed(acc), acc) + } else { + // create a new account + let acc = rng.gen::(); + (RuntimeOrigin::signed(acc), acc) + } +} + +fn random_ed_multiple(rng: &mut R) -> Balance { + let multiple = rng.gen_range(MIN_ED_MULTIPLE..MAX_ED_MULTIPLE); + ExistentialDeposit::get() * multiple +} + +fn fund_account(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(mut rng: &mut R) -> Option { + BondedPools::::iter_keys().collect::>().choose(&mut rng).map(|x| *x) +} + +fn random_call(mut rng: &mut R) -> (pools::Call, RuntimeOrigin) { + let op = rng.gen::(); + let mut op_count = as 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::::join { amount, pool_id }, origin) + }, + 1 => { + // bond_extra + let (origin, who) = random_signed_origin(&mut rng); + let extra = if rng.gen::() { + BondExtra::Rewards + } else { + fund_account(&mut rng, &who); + let amount = random_ed_multiple(&mut rng); + BondExtra::FreeBalance(amount) + }; + (PoolsCall::::bond_extra { extra }, origin) + }, + 2 => { + // claim_payout + let (origin, _) = random_signed_origin(&mut rng); + (PoolsCall::::claim_payout {}, origin) + }, + 3 => { + // unbond + let (origin, who) = random_signed_origin(&mut rng); + let amount = random_ed_multiple(&mut rng); + (PoolsCall::::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::::pool_withdraw_unbonded { pool_id, num_slashing_spans: 0 }, origin) + }, + 5 => { + // withdraw_unbonded + let (origin, who) = random_signed_origin(&mut rng); + ( + PoolsCall::::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; + let bouncer = who; + let nominator = who; + (PoolsCall::::create { amount, root, bouncer, 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::::nominate { pool_id, validators }, origin) + }, + _ => unreachable!(), + } +} + +#[derive(Default)] +struct RewardAgent { + who: AccountId, + pool_id: Option, + 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::::get(); + let amount = 10 * ExistentialDeposit::get(); + let origin = RuntimeOrigin::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:: { 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::::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 = RuntimeOrigin::signed(42); + assert_ok!(PoolsCall::::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; + } +} + +fn main() { + let mut reward_agent = RewardAgent::new(REWARD_AGENT_ACCOUNT); + sp_tracing::try_init_simple(); + let mut ext = sp_io::TestExternalities::new_empty(); + let mut events_histogram = Vec::<(PoolsEvents, u32)>::default(); + let mut iteration = 0 as BlockNumber; + let mut ok = 0; + let mut err = 0; + + let dot: Balance = (10 as Balance).pow(10); + ExistentialDeposit::set(dot); + BondingDuration::set(8); + + ext.execute_with(|| { + MaxPoolMembers::::set(Some(10_000)); + MaxPoolMembersPerPool::::set(Some(1000)); + MaxPools::::set(Some(1_000)); + GlobalMaxCommission::::set(Some(Perbill::from_percent(25))); + + MinCreateBond::::set(10 * ExistentialDeposit::get()); + MinJoinBond::::set(5 * ExistentialDeposit::get()); + System::set_block_number(1); + }); + + loop { + fuzz!(|seed: [u8; 32]| { + use ::rand::{rngs::SmallRng, SeedableRng}; + let mut rng = SmallRng::from_seed(seed); + + ext.execute_with(|| { + let (call, origin) = random_call(&mut rng); + let outcome = call.clone().dispatch_bypass_filter(origin.clone()); + iteration += 1; + match outcome { + Ok(_) => ok += 1, + Err(_) => err += 1, + }; + + log!( + trace, + "iteration {}, call {:?}, origin {:?}, outcome: {:?}, so far {} ok {} err", + iteration, + call, + origin, + outcome, + ok, + err, + ); + + // possibly join the reward_agent + if iteration > ERA / 2 && BondedPools::::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::().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 pallet_nomination_pools::mock::RuntimeEvent::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::::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::::get(id).map(|p| p.points).unwrap(); + let member_points = + PoolMembers::::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::::count(), + PoolMembers::::count(), + ok, + err, + events_histogram + .iter() + .map(|(x, c)| ( + format!("{:?}", x) + .split(" ") + .map(|x| x.to_string()) + .collect::>() + .first() + .cloned() + .unwrap(), + c, + )) + .collect::>(), + ); + } + }) + }) + } +} diff --git a/pallets/nomination-pools/runtime-api/Cargo.toml b/pallets/nomination-pools/runtime-api/Cargo.toml new file mode 100644 index 000000000..c0ba22525 --- /dev/null +++ b/pallets/nomination-pools/runtime-api/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pallet-nomination-pools-runtime-api" +version = "1.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Runtime API for nomination-pools FRAME pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +sp-api = { version = "4.0.0-dev", default-features = false } +sp-std = { version = "8.0.0", default-features = false } +pallet-nomination-pools = { version = "1.0.0", default-features = false, path = "../" } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-api/std", + "sp-std/std", + "pallet-nomination-pools/std", +] diff --git a/pallets/nomination-pools/runtime-api/README.md b/pallets/nomination-pools/runtime-api/README.md new file mode 100644 index 000000000..af90b3173 --- /dev/null +++ b/pallets/nomination-pools/runtime-api/README.md @@ -0,0 +1,3 @@ +Runtime API definition for nomination-pools pallet. + +License: Apache-2.0 \ No newline at end of file diff --git a/pallets/nomination-pools/runtime-api/src/lib.rs b/pallets/nomination-pools/runtime-api/src/lib.rs new file mode 100644 index 000000000..881c3c363 --- /dev/null +++ b/pallets/nomination-pools/runtime-api/src/lib.rs @@ -0,0 +1,42 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Runtime API definition for nomination-pools pallet. +//! Currently supports only one rpc endpoint. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Codec; +use pallet_nomination_pools::PoolId; + +sp_api::decl_runtime_apis! { + /// Runtime api for accessing information about nomination pools. + pub trait NominationPoolsApi + where + AccountId: Codec, + Balance: Codec, + { + /// Returns the pending rewards for the member that the AccountId was given for. + fn pending_rewards(who: AccountId) -> Balance; + + /// Returns the equivalent balance of `points` for a given pool. + fn points_to_balance(pool_id: PoolId, points: Balance) -> Balance; + + /// Returns the equivalent points of `new_funds` for a given pool. + fn balance_to_points(pool_id: PoolId, new_funds: Balance) -> Balance; + } +} diff --git a/pallets/nomination-pools/src/lib.rs b/pallets/nomination-pools/src/lib.rs new file mode 100644 index 000000000..919c5872c --- /dev/null +++ b/pallets/nomination-pools/src/lib.rs @@ -0,0 +1,3296 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! # Nomination Pools for Staking Delegation +//! +//! A pallet that allows members to delegate their stake to nominating pools. A nomination pool acts +//! as nominator and nominates validators on the members behalf. +//! +//! # Index +//! +//! * [Key terms](#key-terms) +//! * [Usage](#usage) +//! * [Implementor's Guide](#implementors-guide) +//! * [Design](#design) +//! +//! ## Key Terms +//! +//! * pool id: A unique identifier of each pool. Set to u32. +//! * bonded pool: Tracks the distribution of actively staked funds. See [`BondedPool`] and +//! [`BondedPoolInner`]. +//! * reward pool: Tracks rewards earned by actively staked funds. See [`RewardPool`] and +//! [`RewardPools`]. +//! * unbonding sub pools: Collection of pools at different phases of the unbonding lifecycle. See +//! [`SubPools`] and [`SubPoolsStorage`]. +//! * members: Accounts that are members of pools. See [`PoolMember`] and [`PoolMembers`]. +//! * roles: Administrative roles of each pool, capable of controlling nomination, and the state of +//! the pool. +//! * point: A unit of measure for a members portion of a pool's funds. Points initially have a +//! ratio of 1 (as set by `POINTS_TO_BALANCE_INIT_RATIO`) to balance, but as slashing happens, +//! this can change. +//! * kick: The act of a pool administrator forcibly ejecting a member. +//! * bonded account: A key-less account id derived from the pool id that acts as the bonded +//! account. This account registers itself as a nominator in the staking system, and follows +//! exactly the same rules and conditions as a normal staker. Its bond increases or decreases as +//! members join, it can `nominate` or `chill`, and might not even earn staking rewards if it is +//! not nominating proper validators. +//! * reward account: A similar key-less account, that is set as the `Payee` account for the bonded +//! account for all staking rewards. +//! * change rate: The rate at which pool commission can be changed. A change rate consists of a +//! `max_increase` and `min_delay`, dictating the maximum percentage increase that can be applied +//! to the commission per number of blocks. +//! * throttle: An attempted commission increase is throttled if the attempted change falls outside +//! the change rate bounds. +//! +//! ## Usage +//! +//! ### Join +//! +//! An account can stake funds with a nomination pool by calling [`Call::join`]. +//! +//! ### Claim rewards +//! +//! After joining a pool, a member can claim rewards by calling [`Call::claim_payout`]. +//! +//! A pool member can also set a `ClaimPermission` with [`Call::set_claim_permission`], to allow +//! other members to permissionlessly bond or withdraw their rewards by calling +//! [`Call::bond_extra_other`] or [`Call::claim_payout_other`] respectively. +//! +//! For design docs see the [reward pool](#reward-pool) section. +//! +//! ### Leave +//! +//! In order to leave, a member must take two steps. +//! +//! First, they must call [`Call::unbond`]. The unbond extrinsic will start the unbonding process by +//! unbonding all or a portion of the members funds. +//! +//! > A member can have up to [`Config::MaxUnbonding`] distinct active unbonding requests. +//! +//! Second, once [`sp_staking::StakingInterface::bonding_duration`] eras have passed, the member can +//! call [`Call::withdraw_unbonded`] to withdraw any funds that are free. +//! +//! For design docs see the [bonded pool](#bonded-pool) and [unbonding sub +//! pools](#unbonding-sub-pools) sections. +//! +//! ### Slashes +//! +//! Slashes are distributed evenly across the bonded pool and the unbonding pools from slash era+1 +//! through the slash apply era. Thus, any member who either +//! +//! 1. unbonded, or +//! 2. was actively bonded +// +//! in the aforementioned range of eras will be affected by the slash. A member is slashed pro-rata +//! based on its stake relative to the total slash amount. +//! +//! Slashing does not change any single member's balance. Instead, the slash will only reduce the +//! balance associated with a particular pool. But, we never change the total *points* of a pool +//! because of slashing. Therefore, when a slash happens, the ratio of points to balance changes in +//! a pool. In other words, the value of one point, which is initially 1-to-1 against a unit of +//! balance, is now less than one balance because of the slash. +//! +//! ### Administration +//! +//! A pool can be created with the [`Call::create`] call. Once created, the pools nominator or root +//! user must call [`Call::nominate`] to start nominating. [`Call::nominate`] can be called at +//! anytime to update validator selection. +//! +//! Similar to [`Call::nominate`], [`Call::chill`] will chill to pool in the staking system, and +//! [`Call::pool_withdraw_unbonded`] will withdraw any unbonding chunks of the pool bonded account. +//! The latter call is permissionless and can be called by anyone at any time. +//! +//! To help facilitate pool administration the pool has one of three states (see [`PoolState`]): +//! +//! * Open: Anyone can join the pool and no members can be permissionlessly removed. +//! * Blocked: No members can join and some admin roles can kick members. Kicking is not instant, +//! and follows the same process of `unbond` and then `withdraw_unbonded`. In other words, +//! administrators can permissionlessly unbond other members. +//! * Destroying: No members can join and all members can be permissionlessly removed with +//! [`Call::unbond`] and [`Call::withdraw_unbonded`]. Once a pool is in destroying state, it +//! cannot be reverted to another state. +//! +//! A pool has 4 administrative roles (see [`PoolRoles`]): +//! +//! * Depositor: creates the pool and is the initial member. They can only leave the pool once all +//! other members have left. Once they fully withdraw their funds, the pool is destroyed. +//! * Nominator: can select which validators the pool nominates. +//! * Bouncer: can change the pools state and kick members if the pool is blocked. +//! * Root: can change the nominator, bouncer, or itself, manage and claim commission, and can +//! perform any of the actions the nominator or bouncer can. +//! +//! ### Commission +//! +//! A pool can optionally have a commission configuration, via the `root` role, set with +//! [`Call::set_commission`] and claimed with [`Call::claim_commission`]. A payee account must be +//! supplied with the desired commission percentage. Beyond the commission itself, a pool can have a +//! maximum commission and a change rate. +//! +//! Importantly, both max commission [`Call::set_commission_max`] and change rate +//! [`Call::set_commission_change_rate`] can not be removed once set, and can only be set to more +//! restrictive values (i.e. a lower max commission or a slower change rate) in subsequent updates. +//! +//! If set, a pool's commission is bound to [`GlobalMaxCommission`] at the time it is applied to +//! pending rewards. [`GlobalMaxCommission`] is intended to be updated only via governance. +//! +//! When a pool is dissolved, any outstanding pending commission that has not been claimed will be +//! transferred to the depositor. +//! +//! Implementation note: Commission is analogous to a separate member account of the pool, with its +//! own reward counter in the form of `current_pending_commission`. +//! +//! Crucially, commission is applied to rewards based on the current commission in effect at the +//! time rewards are transferred into the reward pool. This is to prevent the malicious behaviour of +//! changing the commission rate to a very high value after rewards are accumulated, and thus claim +//! an unexpectedly high chunk of the reward. +//! +//! ### Dismantling +//! +//! As noted, a pool is destroyed once +//! +//! 1. First, all members need to fully unbond and withdraw. If the pool state is set to +//! `Destroying`, this can happen permissionlessly. +//! 2. The depositor itself fully unbonds and withdraws. +//! +//! > Note that at this point, based on the requirements of the staking system, the pool's bonded +//! > account's stake might not be able to ge below a certain threshold as a nominator. At this +//! > point, the pool should `chill` itself to allow the depositor to leave. See [`Call::chill`]. +//! +//! ## Implementor's Guide +//! +//! Some notes and common mistakes that wallets/apps wishing to implement this pallet should be +//! aware of: +//! +//! +//! ### Pool Members +//! +//! * In general, whenever a pool member changes their total point, the chain will automatically +//! claim all their pending rewards for them. This is not optional, and MUST happen for the reward +//! calculation to remain correct (see the documentation of `bond` as an example). So, make sure +//! you are warning your users about it. They might be surprised if they see that they bonded an +//! extra 100 DOTs, and now suddenly their 5.23 DOTs in pending reward is gone. It is not gone, it +//! has been paid out to you! +//! * Joining a pool implies transferring funds to the pool account. So it might be (based on which +//! wallet that you are using) that you no longer see the funds that are moved to the pool in your +//! “free balance” section. Make sure the user is aware of this, and not surprised by seeing this. +//! Also, the transfer that happens here is configured to to never accidentally destroy the sender +//! account. So to join a Pool, your sender account must remain alive with 1 DOT left in it. This +//! means, with 1 DOT as existential deposit, and 1 DOT as minimum to join a pool, you need at +//! least 2 DOT to join a pool. Consequently, if you are suggesting members to join a pool with +//! “Maximum possible value”, you must subtract 1 DOT to remain in the sender account to not +//! accidentally kill it. +//! * Points and balance are not the same! Any pool member, at any point in time, can have points in +//! either the bonded pool or any of the unbonding pools. The crucial fact is that in any of these +//! pools, the ratio of point to balance is different and might not be 1. Each pool starts with a +//! ratio of 1, but as time goes on, for reasons such as slashing, the ratio gets broken. Over +//! time, 100 points in a bonded pool can be worth 90 DOTs. Make sure you are either representing +//! points as points (not as DOTs), or even better, always display both: “You have x points in +//! pool y which is worth z DOTs”. See here and here for examples of how to calculate point to +//! balance ratio of each pool (it is almost trivial ;)) +//! +//! ### Pool Management +//! +//! * The pool will be seen from the perspective of the rest of the system as a single nominator. +//! Ergo, This nominator must always respect the `staking.minNominatorBond` limit. Similar to a +//! normal nominator, who has to first `chill` before fully unbonding, the pool must also do the +//! same. The pool’s bonded account will be fully unbonded only when the depositor wants to leave +//! and dismantle the pool. All that said, the message is: the depositor can only leave the chain +//! when they chill the pool first. +//! +//! ## Design +//! +//! _Notes_: this section uses pseudo code to explain general design and does not necessarily +//! reflect the exact implementation. Additionally, a working knowledge of `pallet-staking`'s api is +//! assumed. +//! +//! ### Goals +//! +//! * Maintain network security by upholding integrity of slashing events, sufficiently penalizing +//! members that where in the pool while it was backing a validator that got slashed. +//! * Maximize scalability in terms of member count. +//! +//! In order to maintain scalability, all operations are independent of the number of members. To do +//! this, delegation specific information is stored local to the member while the pool data +//! structures have bounded datum. +//! +//! ### Bonded pool +//! +//! A bonded pool nominates with its total balance, excluding that which has been withdrawn for +//! unbonding. The total points of a bonded pool are always equal to the sum of points of the +//! delegation members. A bonded pool tracks its points and reads its bonded balance. +//! +//! When a member joins a pool, `amount_transferred` is transferred from the members account to the +//! bonded pools account. Then the pool calls `staking::bond_extra(amount_transferred)` and issues +//! new points which are tracked by the member and added to the bonded pool's points. +//! +//! When the pool already has some balance, we want the value of a point before the transfer to +//! equal the value of a point after the transfer. So, when a member joins a bonded pool with a +//! given `amount_transferred`, we maintain the ratio of bonded balance to points such that: +//! +//! ```text +//! balance_after_transfer / points_after_transfer == balance_before_transfer / points_before_transfer; +//! ``` +//! +//! To achieve this, we issue points based on the following: +//! +//! ```text +//! points_issued = (points_before_transfer / balance_before_transfer) * amount_transferred; +//! ``` +//! +//! For new bonded pools we can set the points issued per balance arbitrarily. In this +//! implementation we use a 1 points to 1 balance ratio for pool creation (see +//! [`POINTS_TO_BALANCE_INIT_RATIO`]). +//! +//! **Relevant extrinsics:** +//! +//! * [`Call::create`] +//! * [`Call::join`] +//! +//! ### Reward pool +//! +//! When a pool is first bonded it sets up a deterministic, inaccessible account as its reward +//! destination. This reward account combined with `RewardPool` compose a reward pool. +//! +//! Reward pools are completely separate entities to bonded pools. Along with its account, a reward +//! pool also tracks its outstanding and claimed rewards as counters, in addition to pending and +//! claimed commission. These counters are updated with `RewardPool::update_records`. The current +//! reward counter of the pool (the total outstanding rewards, in points) is also callable with the +//! `RewardPool::current_reward_counter` method. +//! +//! See [this link](https://hackmd.io/PFGn6wI5TbCmBYoEA_f2Uw) for an in-depth explanation of the +//! reward pool mechanism. +//! +//! **Relevant extrinsics:** +//! +//! * [`Call::claim_payout`] +//! +//! ### Unbonding sub pools +//! +//! When a member unbonds, it's balance is unbonded in the bonded pool's account and tracked in an +//! unbonding pool associated with the active era. If no such pool exists, one is created. To track +//! which unbonding sub pool a member belongs too, a member tracks it's `unbonding_era`. +//! +//! When a member initiates unbonding it's claim on the bonded pool (`balance_to_unbond`) is +//! computed as: +//! +//! ```text +//! balance_to_unbond = (bonded_pool.balance / bonded_pool.points) * member.points; +//! ``` +//! +//! If this is the first transfer into an unbonding pool arbitrary amount of points can be issued +//! per balance. In this implementation unbonding pools are initialized with a 1 point to 1 balance +//! ratio (see [`POINTS_TO_BALANCE_INIT_RATIO`]). Otherwise, the unbonding pools hold the same +//! points to balance ratio properties as the bonded pool, so member points in the unbonding pool +//! are issued based on +//! +//! ```text +//! new_points_issued = (points_before_transfer / balance_before_transfer) * balance_to_unbond; +//! ``` +//! +//! For scalability, a bound is maintained on the number of unbonding sub pools (see +//! [`TotalUnbondingPools`]). An unbonding pool is removed once its older than `current_era - +//! TotalUnbondingPools`. An unbonding pool is merged into the unbonded pool with +//! +//! ```text +//! unbounded_pool.balance = unbounded_pool.balance + unbonding_pool.balance; +//! unbounded_pool.points = unbounded_pool.points + unbonding_pool.points; +//! ``` +//! +//! This scheme "averages" out the points value in the unbonded pool. +//! +//! Once a members `unbonding_era` is older than `current_era - +//! [sp_staking::StakingInterface::bonding_duration]`, it can can cash it's points out of the +//! corresponding unbonding pool. If it's `unbonding_era` is older than `current_era - +//! TotalUnbondingPools`, it can cash it's points from the unbonded pool. +//! +//! **Relevant extrinsics:** +//! +//! * [`Call::unbond`] +//! * [`Call::withdraw_unbonded`] +//! +//! ### Slashing +//! +//! This section assumes that the slash computation is executed by +//! `pallet_staking::StakingLedger::slash`, which passes the information to this pallet via +//! [`sp_staking::OnStakingUpdate::on_slash`]. +//! +//! Unbonding pools need to be slashed to ensure all nominators whom where in the bonded pool while +//! it was backing a validator that equivocated are punished. Without these measures a member could +//! unbond right after a validator equivocated with no consequences. +//! +//! This strategy is unfair to members who joined after the slash, because they get slashed as well, +//! but spares members who unbond. The latter is much more important for security: if a pool's +//! validators are attacking the network, their members need to unbond fast! Avoiding slashes gives +//! them an incentive to do that if validators get repeatedly slashed. +//! +//! To be fair to joiners, this implementation also need joining pools, which are actively staking, +//! in addition to the unbonding pools. For maintenance simplicity these are not implemented. +//! Related: +//! +//! ### Limitations +//! +//! * PoolMembers cannot vote with their staked funds because they are transferred into the pools +//! account. In the future this can be overcome by allowing the members to vote with their bonded +//! funds via vote splitting. +//! * PoolMembers cannot quickly transfer to another pool if they do no like nominations, instead +//! they must wait for the unbonding duration. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Codec; +use frame_support::{ + defensive, ensure, + pallet_prelude::{MaxEncodedLen, *}, + storage::bounded_btree_map::BoundedBTreeMap, + traits::{ + Currency, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, + ExistenceRequirement, Get, + }, + DefaultNoBound, PalletError, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use scale_info::TypeInfo; +use sp_core::U256; +use sp_runtime::{ + traits::{ + AccountIdConversion, Bounded, CheckedAdd, CheckedSub, Convert, Saturating, StaticLookup, + Zero, + }, + FixedPointNumber, Perbill, +}; +use sp_staking::{EraIndex, StakingInterface}; +use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, ops::Div, vec::Vec}; + +#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))] +use sp_runtime::TryRuntimeError; + +/// The log target of this pallet. +pub const LOG_TARGET: &str = "runtime::nomination-pools"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: $crate::LOG_TARGET, + concat!("[{:?}] 🏊‍♂️ ", $patter), >::block_number() $(, $values)* + ) + }; +} + +#[cfg(any(test, feature = "fuzzing"))] +pub mod mock; +#[cfg(test)] +mod tests; + +pub mod migration; +pub mod weights; + +pub use pallet::*; +pub use weights::WeightInfo; + +/// The balance type used by the currency system. +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; +/// Type used for unique identifier of each pool. +pub type PoolId = u32; + +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +pub const POINTS_TO_BALANCE_INIT_RATIO: u32 = 1; + +/// Possible operations on the configuration values of this pallet. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, PartialEq, Clone)] +pub enum ConfigOp { + /// Don't change. + Noop, + /// Set the given value. + Set(T), + /// Remove from storage. + Remove, +} + +/// The type of bonding that can happen to a pool. +enum BondType { + /// Someone is bonding into the pool upon creation. + Create, + /// Someone is adding more funds later to this pool. + Later, +} + +/// How to increase the bond of a member. +#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum BondExtra { + /// Take from the free balance. + FreeBalance(Balance), + /// Take the entire amount from the accumulated rewards. + Rewards, +} + +/// The type of account being created. +#[derive(Encode, Decode)] +enum AccountType { + Bonded, + Reward, +} + +/// The permission a pool member can set for other accounts to claim rewards on their behalf. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum ClaimPermission { + /// Only the pool member themself can claim their rewards. + Permissioned, + /// Anyone can compound rewards on a pool member's behalf. + PermissionlessCompound, + /// Anyone can withdraw rewards on a pool member's behalf. + PermissionlessWithdraw, + /// Anyone can withdraw and compound rewards on a member's behalf. + PermissionlessAll, +} + +impl ClaimPermission { + fn can_bond_extra(&self) -> bool { + matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessCompound) + } + + fn can_claim_payout(&self) -> bool { + matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessWithdraw) + } +} + +impl Default for ClaimPermission { + fn default() -> Self { + Self::Permissioned + } +} + +/// A member in a pool. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, CloneNoBound)] +#[cfg_attr(feature = "std", derive(frame_support::PartialEqNoBound, DefaultNoBound))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct PoolMember { + /// The identifier of the pool to which `who` belongs. + pub pool_id: PoolId, + /// The quantity of points this member has in the bonded pool or in a sub pool if + /// `Self::unbonding_era` is some. + pub points: BalanceOf, + /// The reward counter at the time of this member's last payout claim. + pub last_recorded_reward_counter: T::RewardCounter, + /// The eras in which this member is unbonding, mapped from era index to the number of + /// points scheduled to unbond in the given era. + pub unbonding_eras: BoundedBTreeMap, T::MaxUnbonding>, +} + +impl PoolMember { + /// The pending rewards of this member. + fn pending_rewards( + &self, + current_reward_counter: T::RewardCounter, + ) -> Result, Error> { + // accuracy note: Reward counters are `FixedU128` with base of 10^18. This value is being + // multiplied by a point. The worse case of a point is 10x the granularity of the balance + // (10x is the common configuration of `MaxPointsToBalance`). + // + // Assuming roughly the current issuance of polkadot (12,047,781,394,999,601,455, which is + // 1.2 * 10^9 * 10^10 = 1.2 * 10^19), the worse case point value is around 10^20. + // + // The final multiplication is: + // + // rc * 10^20 / 10^18 = rc * 100 + // + // the implementation of `multiply_by_rational_with_rounding` shows that it will only fail + // if the final division is not enough to fit in u128. In other words, if `rc * 100` is more + // than u128::max. Given that RC is interpreted as reward per unit of point, and unit of + // point is equal to balance (normally), and rewards are usually a proportion of the points + // in the pool, the likelihood of rc reaching near u128::MAX is near impossible. + + (current_reward_counter.defensive_saturating_sub(self.last_recorded_reward_counter)) + .checked_mul_int(self.active_points()) + .ok_or(Error::::OverflowRisk) + } + + /// Active balance of the member. + /// + /// This is derived from the ratio of points in the pool to which the member belongs to. + /// Might return different values based on the pool state for the same member and points. + fn active_balance(&self) -> BalanceOf { + if let Some(pool) = BondedPool::::get(self.pool_id).defensive() { + pool.points_to_balance(self.points) + } else { + Zero::zero() + } + } + + /// Total points of this member, both active and unbonding. + fn total_points(&self) -> BalanceOf { + self.active_points().saturating_add(self.unbonding_points()) + } + + /// Active points of the member. + fn active_points(&self) -> BalanceOf { + self.points + } + + /// Inactive points of the member, waiting to be withdrawn. + fn unbonding_points(&self) -> BalanceOf { + self.unbonding_eras + .as_ref() + .iter() + .fold(BalanceOf::::zero(), |acc, (_, v)| acc.saturating_add(*v)) + } + + /// Try and unbond `points_dissolved` from self, and in return mint `points_issued` into the + /// corresponding `era`'s unlock schedule. + /// + /// In the absence of slashing, these two points are always the same. In the presence of + /// slashing, the value of points in different pools varies. + /// + /// Returns `Ok(())` and updates `unbonding_eras` and `points` if success, `Err(_)` otherwise. + fn try_unbond( + &mut self, + points_dissolved: BalanceOf, + points_issued: BalanceOf, + unbonding_era: EraIndex, + ) -> Result<(), Error> { + if let Some(new_points) = self.points.checked_sub(&points_dissolved) { + match self.unbonding_eras.get_mut(&unbonding_era) { + Some(already_unbonding_points) => + *already_unbonding_points = + already_unbonding_points.saturating_add(points_issued), + None => self + .unbonding_eras + .try_insert(unbonding_era, points_issued) + .map(|old| { + if old.is_some() { + defensive!("value checked to not exist in the map; qed"); + } + }) + .map_err(|_| Error::::MaxUnbondingLimit)?, + } + self.points = new_points; + Ok(()) + } else { + Err(Error::::MinimumBondNotMet) + } + } + + /// Withdraw any funds in [`Self::unbonding_eras`] who's deadline in reached and is fully + /// unlocked. + /// + /// Returns a a subset of [`Self::unbonding_eras`] that got withdrawn. + /// + /// Infallible, noop if no unbonding eras exist. + fn withdraw_unlocked( + &mut self, + current_era: EraIndex, + ) -> BoundedBTreeMap, T::MaxUnbonding> { + // NOTE: if only drain-filter was stable.. + let mut removed_points = + BoundedBTreeMap::, T::MaxUnbonding>::default(); + self.unbonding_eras.retain(|e, p| { + if *e > current_era { + true + } else { + removed_points + .try_insert(*e, *p) + .expect("source map is bounded, this is a subset, will be bounded; qed"); + false + } + }); + removed_points + } +} + +/// A pool's possible states. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, RuntimeDebugNoBound, Clone, Copy)] +pub enum PoolState { + /// The pool is open to be joined, and is working normally. + Open, + /// The pool is blocked. No one else can join. + Blocked, + /// The pool is in the process of being destroyed. + /// + /// All members can now be permissionlessly unbonded, and the pool can never go back to any + /// other state other than being dissolved. + Destroying, +} + +/// Pool administration roles. +/// +/// Any pool has a depositor, which can never change. But, all the other roles are optional, and +/// cannot exist. Note that if `root` is set to `None`, it basically means that the roles of this +/// pool can never change again (except via governance). +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Clone)] +pub struct PoolRoles { + /// Creates the pool and is the initial member. They can only leave the pool once all other + /// members have left. Once they fully leave, the pool is destroyed. + pub depositor: AccountId, + /// Can change the nominator, bouncer, or itself and can perform any of the actions the + /// nominator or bouncer can. + pub root: Option, + /// Can select which validators the pool nominates. + pub nominator: Option, + /// Can change the pools state and kick members if the pool is blocked. + pub bouncer: Option, +} + +/// Pool commission. +/// +/// The pool `root` can set commission configuration after pool creation. By default, all commission +/// values are `None`. Pool `root` can also set `max` and `change_rate` configurations before +/// setting an initial `current` commission. +/// +/// `current` is a tuple of the commission percentage and payee of commission. `throttle_from` +/// keeps track of which block `current` was last updated. A `max` commission value can only be +/// decreased after the initial value is set, to prevent commission from repeatedly increasing. +/// +/// An optional commission `change_rate` allows the pool to set strict limits to how much commission +/// can change in each update, and how often updates can take place. +#[derive( + Encode, Decode, DefaultNoBound, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Copy, Clone, +)] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct Commission { + /// Optional commission rate of the pool along with the account commission is paid to. + pub current: Option<(Perbill, T::AccountId)>, + /// Optional maximum commission that can be set by the pool `root`. Once set, this value can + /// only be updated to a decreased value. + pub max: Option, + /// Optional configuration around how often commission can be updated, and when the last + /// commission update took place. + pub change_rate: Option>>, + /// The block from where throttling should be checked from. This value will be updated on all + /// commission updates and when setting an initial `change_rate`. + pub throttle_from: Option>, +} + +impl Commission { + /// Returns true if the current commission updating to `to` would exhaust the change rate + /// limits. + /// + /// A commission update will be throttled (disallowed) if: + /// 1. not enough blocks have passed since the `throttle_from` block, if exists, or + /// 2. the new commission is greater than the maximum allowed increase. + fn throttling(&self, to: &Perbill) -> bool { + if let Some(t) = self.change_rate.as_ref() { + let commission_as_percent = + self.current.as_ref().map(|(x, _)| *x).unwrap_or(Perbill::zero()); + + // do not throttle if `to` is the same or a decrease in commission. + if *to <= commission_as_percent { + return false + } + // Test for `max_increase` throttling. + // + // Throttled if the attempted increase in commission is greater than `max_increase`. + if (*to).saturating_sub(commission_as_percent) > t.max_increase { + return true + } + + // Test for `min_delay` throttling. + // + // Note: matching `None` is defensive only. `throttle_from` should always exist where + // `change_rate` has already been set, so this scenario should never happen. + return self.throttle_from.map_or_else( + || { + defensive!("throttle_from should exist if change_rate is set"); + true + }, + |f| { + // if `min_delay` is zero (no delay), not throttling. + if t.min_delay == Zero::zero() { + false + } else { + // throttling if blocks passed is less than `min_delay`. + let blocks_surpassed = + >::block_number().saturating_sub(f); + blocks_surpassed < t.min_delay + } + }, + ) + } + false + } + + /// Gets the pool's current commission, or returns Perbill::zero if none is set. + /// Bounded to global max if current is greater than `GlobalMaxCommission`. + fn current(&self) -> Perbill { + self.current + .as_ref() + .map_or(Perbill::zero(), |(c, _)| *c) + .min(GlobalMaxCommission::::get().unwrap_or(Bounded::max_value())) + } + + /// Set the pool's commission. + /// + /// Update commission based on `current`. If a `None` is supplied, allow the commission to be + /// removed without any change rate restrictions. Updates `throttle_from` to the current block. + /// If the supplied commission is zero, `None` will be inserted and `payee` will be ignored. + fn try_update_current(&mut self, current: &Option<(Perbill, T::AccountId)>) -> DispatchResult { + self.current = match current { + None => None, + Some((commission, payee)) => { + ensure!(!self.throttling(commission), Error::::CommissionChangeThrottled); + ensure!( + commission <= &GlobalMaxCommission::::get().unwrap_or(Bounded::max_value()), + Error::::CommissionExceedsGlobalMaximum + ); + ensure!( + self.max.map_or(true, |m| commission <= &m), + Error::::CommissionExceedsMaximum + ); + if commission.is_zero() { + None + } else { + Some((*commission, payee.clone())) + } + }, + }; + self.register_update(); + Ok(()) + } + + /// Set the pool's maximum commission. + /// + /// The pool's maximum commission can initially be set to any value, and only smaller values + /// thereafter. If larger values are attempted, this function will return a dispatch error. + /// + /// If `current.0` is larger than the updated max commission value, `current.0` will also be + /// updated to the new maximum. This will also register a `throttle_from` update. + /// A `PoolCommissionUpdated` event is triggered if `current.0` is updated. + fn try_update_max(&mut self, pool_id: PoolId, new_max: Perbill) -> DispatchResult { + ensure!( + new_max <= GlobalMaxCommission::::get().unwrap_or(Bounded::max_value()), + Error::::CommissionExceedsGlobalMaximum + ); + if let Some(old) = self.max.as_mut() { + if new_max > *old { + return Err(Error::::MaxCommissionRestricted.into()) + } + *old = new_max; + } else { + self.max = Some(new_max) + }; + let updated_current = self + .current + .as_mut() + .map(|(c, _)| { + let u = *c > new_max; + *c = (*c).min(new_max); + u + }) + .unwrap_or(false); + + if updated_current { + if let Some((_, payee)) = self.current.as_ref() { + Pallet::::deposit_event(Event::::PoolCommissionUpdated { + pool_id, + current: Some((new_max, payee.clone())), + }); + } + self.register_update(); + } + Ok(()) + } + + /// Set the pool's commission `change_rate`. + /// + /// Once a change rate configuration has been set, only more restrictive values can be set + /// thereafter. These restrictions translate to increased `min_delay` values and decreased + /// `max_increase` values. + /// + /// Update `throttle_from` to the current block upon setting change rate for the first time, so + /// throttling can be checked from this block. + fn try_update_change_rate( + &mut self, + change_rate: CommissionChangeRate>, + ) -> DispatchResult { + ensure!(!&self.less_restrictive(&change_rate), Error::::CommissionChangeRateNotAllowed); + + if self.change_rate.is_none() { + self.register_update(); + } + self.change_rate = Some(change_rate); + Ok(()) + } + + /// Updates a commission's `throttle_from` field to the current block. + fn register_update(&mut self) { + self.throttle_from = Some(>::block_number()); + } + + /// Checks whether a change rate is less restrictive than the current change rate, if any. + /// + /// No change rate will always be less restrictive than some change rate, so where no + /// `change_rate` is currently set, `false` is returned. + fn less_restrictive(&self, new: &CommissionChangeRate>) -> bool { + self.change_rate + .as_ref() + .map(|c| new.max_increase > c.max_increase || new.min_delay < c.min_delay) + .unwrap_or(false) + } +} + +/// Pool commission change rate preferences. +/// +/// The pool root is able to set a commission change rate for their pool. A commission change rate +/// consists of 2 values; (1) the maximum allowed commission change, and (2) the minimum amount of +/// blocks that must elapse before commission updates are allowed again. +/// +/// Commission change rates are not applied to decreases in commission. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Copy, Clone)] +pub struct CommissionChangeRate { + /// The maximum amount the commission can be updated by per `min_delay` period. + pub max_increase: Perbill, + /// How often an update can take place. + pub min_delay: BlockNumber, +} + +/// Pool permissions and state +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Clone)] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct BondedPoolInner { + /// The commission rate of the pool. + pub commission: Commission, + /// Count of members that belong to the pool. + pub member_counter: u32, + /// Total points of all the members in the pool who are actively bonded. + pub points: BalanceOf, + /// See [`PoolRoles`]. + pub roles: PoolRoles, + /// The current state of the pool. + pub state: PoolState, +} + +/// A wrapper for bonded pools, with utility functions. +/// +/// The main purpose of this is to wrap a [`BondedPoolInner`], with the account +/// + id of the pool, for easier access. +#[derive(RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq))] +pub struct BondedPool { + /// The identifier of the pool. + id: PoolId, + /// The inner fields. + inner: BondedPoolInner, +} + +impl sp_std::ops::Deref for BondedPool { + type Target = BondedPoolInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl sp_std::ops::DerefMut for BondedPool { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl BondedPool { + /// Create a new bonded pool with the given roles and identifier. + fn new(id: PoolId, roles: PoolRoles) -> Self { + Self { + id, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: Zero::zero(), + points: Zero::zero(), + roles, + state: PoolState::Open, + }, + } + } + + /// Get [`Self`] from storage. Returns `None` if no entry for `pool_account` exists. + pub fn get(id: PoolId) -> Option { + BondedPools::::try_get(id).ok().map(|inner| Self { id, inner }) + } + + /// Get the bonded account id of this pool. + fn bonded_account(&self) -> T::AccountId { + Pallet::::create_bonded_account(self.id) + } + + /// Get the reward account id of this pool. + fn reward_account(&self) -> T::AccountId { + Pallet::::create_reward_account(self.id) + } + + /// Consume self and put into storage. + fn put(self) { + BondedPools::::insert(self.id, self.inner); + } + + /// Consume self and remove from storage. + fn remove(self) { + BondedPools::::remove(self.id); + } + + /// Convert the given amount of balance to points given the current pool state. + /// + /// This is often used for bonding and issuing new funds into the pool. + fn balance_to_point(&self, new_funds: BalanceOf) -> BalanceOf { + let bonded_balance = + T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero()); + Pallet::::balance_to_point(bonded_balance, self.points, new_funds) + } + + /// Convert the given number of points to balance given the current pool state. + /// + /// This is often used for unbonding. + fn points_to_balance(&self, points: BalanceOf) -> BalanceOf { + let bonded_balance = + T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero()); + Pallet::::point_to_balance(bonded_balance, self.points, points) + } + + /// Issue points to [`Self`] for `new_funds`. + fn issue(&mut self, new_funds: BalanceOf) -> BalanceOf { + let points_to_issue = self.balance_to_point(new_funds); + self.points = self.points.saturating_add(points_to_issue); + points_to_issue + } + + /// Dissolve some points from the pool i.e. unbond the given amount of points from this pool. + /// This is the opposite of issuing some funds into the pool. + /// + /// Mutates self in place, but does not write anything to storage. + /// + /// Returns the equivalent balance amount that actually needs to get unbonded. + fn dissolve(&mut self, points: BalanceOf) -> BalanceOf { + // NOTE: do not optimize by removing `balance`. it must be computed before mutating + // `self.point`. + let balance = self.points_to_balance(points); + self.points = self.points.saturating_sub(points); + balance + } + + /// Increment the member counter. Ensures that the pool and system member limits are + /// respected. + fn try_inc_members(&mut self) -> Result<(), DispatchError> { + ensure!( + MaxPoolMembersPerPool::::get() + .map_or(true, |max_per_pool| self.member_counter < max_per_pool), + Error::::MaxPoolMembers + ); + ensure!( + MaxPoolMembers::::get().map_or(true, |max| PoolMembers::::count() < max), + Error::::MaxPoolMembers + ); + self.member_counter = self.member_counter.checked_add(1).ok_or(Error::::OverflowRisk)?; + Ok(()) + } + + /// Decrement the member counter. + fn dec_members(mut self) -> Self { + self.member_counter = self.member_counter.defensive_saturating_sub(1); + self + } + + /// The pools balance that is transferrable. + fn transferrable_balance(&self) -> BalanceOf { + let account = self.bonded_account(); + T::Currency::free_balance(&account) + .saturating_sub(T::Staking::active_stake(&account).unwrap_or_default()) + } + + fn is_root(&self, who: &T::AccountId) -> bool { + self.roles.root.as_ref().map_or(false, |root| root == who) + } + + fn is_bouncer(&self, who: &T::AccountId) -> bool { + self.roles.bouncer.as_ref().map_or(false, |bouncer| bouncer == who) + } + + fn can_update_roles(&self, who: &T::AccountId) -> bool { + self.is_root(who) + } + + fn can_nominate(&self, who: &T::AccountId) -> bool { + self.is_root(who) || + self.roles.nominator.as_ref().map_or(false, |nominator| nominator == who) + } + + fn can_kick(&self, who: &T::AccountId) -> bool { + self.state == PoolState::Blocked && (self.is_root(who) || self.is_bouncer(who)) + } + + fn can_toggle_state(&self, who: &T::AccountId) -> bool { + (self.is_root(who) || self.is_bouncer(who)) && !self.is_destroying() + } + + fn can_set_metadata(&self, who: &T::AccountId) -> bool { + self.is_root(who) || self.is_bouncer(who) + } + + fn can_manage_commission(&self, who: &T::AccountId) -> bool { + self.is_root(who) + } + + fn is_destroying(&self) -> bool { + matches!(self.state, PoolState::Destroying) + } + + fn is_destroying_and_only_depositor(&self, alleged_depositor_points: BalanceOf) -> bool { + // we need to ensure that `self.member_counter == 1` as well, because the depositor's + // initial `MinCreateBond` (or more) is what guarantees that the ledger of the pool does not + // get killed in the staking system, and that it does not fall below `MinimumNominatorBond`, + // which could prevent other non-depositor members from fully leaving. Thus, all members + // must withdraw, then depositor can unbond, and finally withdraw after waiting another + // cycle. + self.is_destroying() && self.points == alleged_depositor_points && self.member_counter == 1 + } + + /// Whether or not the pool is ok to be in `PoolSate::Open`. If this returns an `Err`, then the + /// pool is unrecoverable and should be in the destroying state. + fn ok_to_be_open(&self) -> Result<(), DispatchError> { + ensure!(!self.is_destroying(), Error::::CanNotChangeState); + + let bonded_balance = + T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero()); + ensure!(!bonded_balance.is_zero(), Error::::OverflowRisk); + + let points_to_balance_ratio_floor = self + .points + // We checked for zero above + .div(bonded_balance); + + let max_points_to_balance = T::MaxPointsToBalance::get(); + + // Pool points can inflate relative to balance, but only if the pool is slashed. + // If we cap the ratio of points:balance so one cannot join a pool that has been slashed + // by `max_points_to_balance`%, if not zero. + ensure!( + points_to_balance_ratio_floor < max_points_to_balance.into(), + Error::::OverflowRisk + ); + + // then we can be decently confident the bonding pool points will not overflow + // `BalanceOf`. Note that these are just heuristics. + + Ok(()) + } + + /// Check that the pool can accept a member with `new_funds`. + fn ok_to_join(&self) -> Result<(), DispatchError> { + ensure!(self.state == PoolState::Open, Error::::NotOpen); + self.ok_to_be_open()?; + Ok(()) + } + + fn ok_to_unbond_with( + &self, + caller: &T::AccountId, + target_account: &T::AccountId, + target_member: &PoolMember, + unbonding_points: BalanceOf, + ) -> Result<(), DispatchError> { + let is_permissioned = caller == target_account; + let is_depositor = *target_account == self.roles.depositor; + let is_full_unbond = unbonding_points == target_member.active_points(); + + let balance_after_unbond = { + let new_depositor_points = + target_member.active_points().saturating_sub(unbonding_points); + let mut target_member_after_unbond = (*target_member).clone(); + target_member_after_unbond.points = new_depositor_points; + target_member_after_unbond.active_balance() + }; + + // any partial unbonding is only ever allowed if this unbond is permissioned. + ensure!( + is_permissioned || is_full_unbond, + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // any unbond must comply with the balance condition: + ensure!( + is_full_unbond || + balance_after_unbond >= + if is_depositor { + Pallet::::depositor_min_bond() + } else { + MinJoinBond::::get() + }, + Error::::MinimumBondNotMet + ); + + // additional checks: + match (is_permissioned, is_depositor) { + (true, false) => (), + (true, true) => { + // permission depositor unbond: if destroying and pool is empty, always allowed, + // with no additional limits. + if self.is_destroying_and_only_depositor(target_member.active_points()) { + // everything good, let them unbond anything. + } else { + // depositor cannot fully unbond yet. + ensure!(!is_full_unbond, Error::::MinimumBondNotMet); + } + }, + (false, false) => { + // If the pool is blocked, then an admin with kicking permissions can remove a + // member. If the pool is being destroyed, anyone can remove a member + debug_assert!(is_full_unbond); + ensure!( + self.can_kick(caller) || self.is_destroying(), + Error::::NotKickerOrDestroying + ) + }, + (false, true) => { + // the depositor can simply not be unbonded permissionlessly, period. + return Err(Error::::DoesNotHavePermission.into()) + }, + }; + + Ok(()) + } + + /// # Returns + /// + /// * Ok(()) if [`Call::withdraw_unbonded`] can be called, `Err(DispatchError)` otherwise. + fn ok_to_withdraw_unbonded_with( + &self, + caller: &T::AccountId, + target_account: &T::AccountId, + ) -> Result<(), DispatchError> { + // This isn't a depositor + let is_permissioned = caller == target_account; + ensure!( + is_permissioned || self.can_kick(caller) || self.is_destroying(), + Error::::NotKickerOrDestroying + ); + Ok(()) + } + + /// Bond exactly `amount` from `who`'s funds into this pool. + /// + /// If the bond type is `Create`, `Staking::bond` is called, and `who` + /// is allowed to be killed. Otherwise, `Staking::bond_extra` is called and `who` + /// cannot be killed. + /// + /// Returns `Ok(points_issues)`, `Err` otherwise. + fn try_bond_funds( + &mut self, + who: &T::AccountId, + amount: BalanceOf, + ty: BondType, + ) -> Result, DispatchError> { + // Cache the value + let bonded_account = self.bonded_account(); + T::Currency::transfer( + who, + &bonded_account, + amount, + match ty { + BondType::Create => ExistenceRequirement::AllowDeath, + BondType::Later => ExistenceRequirement::KeepAlive, + }, + )?; + // We must calculate the points issued *before* we bond who's funds, else points:balance + // ratio will be wrong. + let points_issued = self.issue(amount); + + match ty { + BondType::Create => T::Staking::bond(&bonded_account, amount, &self.reward_account())?, + // The pool should always be created in such a way its in a state to bond extra, but if + // the active balance is slashed below the minimum bonded or the account cannot be + // found, we exit early. + BondType::Later => T::Staking::bond_extra(&bonded_account, amount)?, + } + + Ok(points_issued) + } + + // Set the state of `self`, and deposit an event if the state changed. State should never be set + // directly in in order to ensure a state change event is always correctly deposited. + fn set_state(&mut self, state: PoolState) { + if self.state != state { + self.state = state; + Pallet::::deposit_event(Event::::StateChanged { + pool_id: self.id, + new_state: state, + }); + }; + } +} + +/// A reward pool. +/// +/// A reward pool is not so much a pool anymore, since it does not contain any shares or points. +/// Rather, simply to fit nicely next to bonded pool and unbonding pools in terms of terminology. In +/// reality, a reward pool is just a container for a few pool-dependent data related to the rewards. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, DefaultNoBound))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct RewardPool { + /// The last recorded value of the reward counter. + /// + /// This is updated ONLY when the points in the bonded pool change, which means `join`, + /// `bond_extra` and `unbond`, all of which is done through `update_recorded`. + last_recorded_reward_counter: T::RewardCounter, + /// The last recorded total payouts of the reward pool. + /// + /// Payouts is essentially income of the pool. + /// + /// Update criteria is same as that of `last_recorded_reward_counter`. + last_recorded_total_payouts: BalanceOf, + /// Total amount that this pool has paid out so far to the members. + total_rewards_claimed: BalanceOf, + /// The amount of commission pending to be claimed. + total_commission_pending: BalanceOf, + /// The amount of commission that has been claimed. + total_commission_claimed: BalanceOf, +} + +impl RewardPool { + /// Getter for [`RewardPool::last_recorded_reward_counter`]. + pub(crate) fn last_recorded_reward_counter(&self) -> T::RewardCounter { + self.last_recorded_reward_counter + } + + /// Register some rewards that are claimed from the pool by the members. + fn register_claimed_reward(&mut self, reward: BalanceOf) { + self.total_rewards_claimed = self.total_rewards_claimed.saturating_add(reward); + } + + /// Update the recorded values of the reward pool. + /// + /// This function MUST be called whenever the points in the bonded pool change, AND whenever the + /// the pools commission is updated. The reason for the former is that a change in pool points + /// will alter the share of the reward balance among pool members, and the reason for the latter + /// is that a change in commission will alter the share of the reward balance among the pool. + fn update_records( + &mut self, + id: PoolId, + bonded_points: BalanceOf, + commission: Perbill, + ) -> Result<(), Error> { + let balance = Self::current_balance(id); + + let (current_reward_counter, new_pending_commission) = + self.current_reward_counter(id, bonded_points, commission)?; + + // Store the reward counter at the time of this update. This is used in subsequent calls to + // `current_reward_counter`, whereby newly pending rewards (in points) are added to this + // value. + self.last_recorded_reward_counter = current_reward_counter; + + // Add any new pending commission that has been calculated from `current_reward_counter` to + // determine the total pending commission at the time of this update. + self.total_commission_pending = + self.total_commission_pending.saturating_add(new_pending_commission); + + // Store the total payouts at the time of this update. Total payouts are essentially the + // entire historical balance of the reward pool, equating to the current balance + the total + // rewards that have left the pool + the total commission that has left the pool. + self.last_recorded_total_payouts = balance + .checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed)) + .ok_or(Error::::OverflowRisk)?; + + Ok(()) + } + + /// Get the current reward counter, based on the given `bonded_points` being the state of the + /// bonded pool at this time. + fn current_reward_counter( + &self, + id: PoolId, + bonded_points: BalanceOf, + commission: Perbill, + ) -> Result<(T::RewardCounter, BalanceOf), Error> { + let balance = Self::current_balance(id); + + // Calculate the current payout balance. The first 3 values of this calculation added + // together represent what the balance would be if no payouts were made. The + // `last_recorded_total_payouts` is then subtracted from this value to cancel out previously + // recorded payouts, leaving only the remaining payouts that have not been claimed. + let current_payout_balance = balance + .saturating_add(self.total_rewards_claimed) + .saturating_add(self.total_commission_claimed) + .saturating_sub(self.last_recorded_total_payouts); + + // Split the `current_payout_balance` into claimable rewards and claimable commission + // according to the current commission rate. + let new_pending_commission = commission * current_payout_balance; + let new_pending_rewards = current_payout_balance.saturating_sub(new_pending_commission); + + // * accuracy notes regarding the multiplication in `checked_from_rational`: + // `current_payout_balance` is a subset of the total_issuance at the very worse. + // `bonded_points` are similarly, in a non-slashed pool, have the same granularity as + // balance, and are thus below within the range of total_issuance. In the worse case + // scenario, for `saturating_from_rational`, we have: + // + // dot_total_issuance * 10^18 / `minJoinBond` + // + // assuming `MinJoinBond == ED` + // + // dot_total_issuance * 10^18 / 10^10 = dot_total_issuance * 10^8 + // + // which, with the current numbers, is a miniscule fraction of the u128 capacity. + // + // Thus, adding two values of type reward counter should be safe for ages in a chain like + // Polkadot. The important note here is that `reward_pool.last_recorded_reward_counter` only + // ever accumulates, but its semantics imply that it is less than total_issuance, when + // represented as `FixedU128`, which means it is less than `total_issuance * 10^18`. + // + // * accuracy notes regarding `checked_from_rational` collapsing to zero, meaning that no + // reward can be claimed: + // + // largest `bonded_points`, such that the reward counter is non-zero, with `FixedU128` will + // be when the payout is being computed. This essentially means `payout/bonded_points` needs + // to be more than 1/1^18. Thus, assuming that `bonded_points` will always be less than `10 + // * dot_total_issuance`, if the reward_counter is the smallest possible value, the value of + // the + // reward being calculated is: + // + // x / 10^20 = 1/ 10^18 + // + // x = 100 + // + // which is basically 10^-8 DOTs. See `smallest_claimable_reward` for an example of this. + let current_reward_counter = + T::RewardCounter::checked_from_rational(new_pending_rewards, bonded_points) + .and_then(|ref r| self.last_recorded_reward_counter.checked_add(r)) + .ok_or(Error::::OverflowRisk)?; + + Ok((current_reward_counter, new_pending_commission)) + } + + /// Current free balance of the reward pool. + /// + /// This is sum of all the rewards that are claimable by pool members. + fn current_balance(id: PoolId) -> BalanceOf { + T::Currency::free_balance(&Pallet::::create_reward_account(id)) + .saturating_sub(T::Currency::minimum_balance()) + } +} + +/// An unbonding pool. This is always mapped with an era. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DefaultNoBound, RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct UnbondPool { + /// The points in this pool. + points: BalanceOf, + /// The funds in the pool. + balance: BalanceOf, +} + +impl UnbondPool { + fn balance_to_point(&self, new_funds: BalanceOf) -> BalanceOf { + Pallet::::balance_to_point(self.balance, self.points, new_funds) + } + + fn point_to_balance(&self, points: BalanceOf) -> BalanceOf { + Pallet::::point_to_balance(self.balance, self.points, points) + } + + /// Issue the equivalent points of `new_funds` into self. + /// + /// Returns the actual amounts of points issued. + fn issue(&mut self, new_funds: BalanceOf) -> BalanceOf { + let new_points = self.balance_to_point(new_funds); + self.points = self.points.saturating_add(new_points); + self.balance = self.balance.saturating_add(new_funds); + new_points + } + + /// Dissolve some points from the unbonding pool, reducing the balance of the pool + /// proportionally. + /// + /// This is the opposite of `issue`. + /// + /// Returns the actual amount of `Balance` that was removed from the pool. + fn dissolve(&mut self, points: BalanceOf) -> BalanceOf { + let balance_to_unbond = self.point_to_balance(points); + self.points = self.points.saturating_sub(points); + self.balance = self.balance.saturating_sub(balance_to_unbond); + + balance_to_unbond + } +} + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DefaultNoBound, RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct SubPools { + /// A general, era agnostic pool of funds that have fully unbonded. The pools + /// of `Self::with_era` will lazily be merged into into this pool if they are + /// older then `current_era - TotalUnbondingPools`. + no_era: UnbondPool, + /// Map of era in which a pool becomes unbonded in => unbond pools. + with_era: BoundedBTreeMap, TotalUnbondingPools>, +} + +impl SubPools { + /// Merge the oldest `with_era` unbond pools into the `no_era` unbond pool. + /// + /// This is often used whilst getting the sub-pool from storage, thus it consumes and returns + /// `Self` for ergonomic purposes. + fn maybe_merge_pools(mut self, current_era: EraIndex) -> Self { + // Ex: if `TotalUnbondingPools` is 5 and current era is 10, we only want to retain pools + // 6..=10. Note that in the first few eras where `checked_sub` is `None`, we don't remove + // anything. + if let Some(newest_era_to_remove) = + current_era.checked_sub(T::PostUnbondingPoolsWindow::get()) + { + self.with_era.retain(|k, v| { + if *k > newest_era_to_remove { + // keep + true + } else { + // merge into the no-era pool + self.no_era.points = self.no_era.points.saturating_add(v.points); + self.no_era.balance = self.no_era.balance.saturating_add(v.balance); + false + } + }); + } + + self + } + + /// The sum of all unbonding balance, regardless of whether they are actually unlocked or not. + #[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))] + fn sum_unbonding_balance(&self) -> BalanceOf { + self.no_era.balance.saturating_add( + self.with_era + .values() + .fold(BalanceOf::::zero(), |acc, pool| acc.saturating_add(pool.balance)), + ) + } +} + +/// The maximum amount of eras an unbonding pool can exist prior to being merged with the +/// `no_era` pool. This is guaranteed to at least be equal to the staking `UnbondingDuration`. For +/// improved UX [`Config::PostUnbondingPoolsWindow`] should be configured to a non-zero value. +pub struct TotalUnbondingPools(PhantomData); +impl Get for TotalUnbondingPools { + fn get() -> u32 { + // NOTE: this may be dangerous in the scenario bonding_duration gets decreased because + // we would no longer be able to decode `BoundedBTreeMap::, + // TotalUnbondingPools>`, which uses `TotalUnbondingPools` as the bound + T::Staking::bonding_duration() + T::PostUnbondingPoolsWindow::get() + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::traits::StorageVersion; + use frame_system::{ensure_signed, pallet_prelude::*}; + use sp_runtime::Perbill; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: weights::WeightInfo; + + /// The nominating balance. + type Currency: Currency; + + /// The type that is used for reward counter. + /// + /// The arithmetic of the reward counter might saturate based on the size of the + /// `Currency::Balance`. If this happens, operations fails. Nonetheless, this type should be + /// chosen such that this failure almost never happens, as if it happens, the pool basically + /// needs to be dismantled (or all pools migrated to a larger `RewardCounter` type, which is + /// a PITA to do). + /// + /// See the inline code docs of `Member::pending_rewards` and `RewardPool::update_recorded` + /// for example analysis. A [`sp_runtime::FixedU128`] should be fine for chains with balance + /// types similar to that of Polkadot and Kusama, in the absence of severe slashing (or + /// prevented via a reasonable `MaxPointsToBalance`), for many many years to come. + type RewardCounter: FixedPointNumber + MaxEncodedLen + TypeInfo + Default + codec::FullCodec; + + /// The nomination pool's pallet id. + #[pallet::constant] + type PalletId: Get; + + /// The maximum pool points-to-balance ratio that an `open` pool can have. + /// + /// This is important in the event slashing takes place and the pool's points-to-balance + /// ratio becomes disproportional. + /// + /// Moreover, this relates to the `RewardCounter` type as well, as the arithmetic operations + /// are a function of number of points, and by setting this value to e.g. 10, you ensure + /// that the total number of points in the system are at most 10 times the total_issuance of + /// the chain, in the absolute worse case. + /// + /// For a value of 10, the threshold would be a pool points-to-balance ratio of 10:1. + /// Such a scenario would also be the equivalent of the pool being 90% slashed. + #[pallet::constant] + type MaxPointsToBalance: Get; + + /// Infallible method for converting `Currency::Balance` to `U256`. + type BalanceToU256: Convert, U256>; + + /// Infallible method for converting `U256` to `Currency::Balance`. + type U256ToBalance: Convert>; + + /// The interface for nominating. + type Staking: StakingInterface, AccountId = Self::AccountId>; + + /// The amount of eras a `SubPools::with_era` pool can exist before it gets merged into the + /// `SubPools::no_era` pool. In other words, this is the amount of eras a member will be + /// able to withdraw from an unbonding pool which is guaranteed to have the correct ratio of + /// points to balance; once the `with_era` pool is merged into the `no_era` pool, the ratio + /// can become skewed due to some slashed ratio getting merged in at some point. + type PostUnbondingPoolsWindow: Get; + + /// The maximum length, in bytes, that a pools metadata maybe. + type MaxMetadataLen: Get; + + /// The maximum number of simultaneous unbonding chunks that can exist per member. + type MaxUnbonding: Get; + } + + /// Minimum amount to bond to join a pool. + #[pallet::storage] + pub type MinJoinBond = StorageValue<_, BalanceOf, ValueQuery>; + + /// Minimum bond required to create a pool. + /// + /// This is the amount that the depositor must put as their initial stake in the pool, as an + /// indication of "skin in the game". + /// + /// This is the value that will always exist in the staking ledger of the pool bonded account + /// while all other accounts leave. + #[pallet::storage] + pub type MinCreateBond = StorageValue<_, BalanceOf, ValueQuery>; + + /// Maximum number of nomination pools that can exist. If `None`, then an unbounded number of + /// pools can exist. + #[pallet::storage] + pub type MaxPools = StorageValue<_, u32, OptionQuery>; + + /// Maximum number of members that can exist in the system. If `None`, then the count + /// members are not bound on a system wide basis. + #[pallet::storage] + pub type MaxPoolMembers = StorageValue<_, u32, OptionQuery>; + + /// Maximum number of members that may belong to pool. If `None`, then the count of + /// members is not bound on a per pool basis. + #[pallet::storage] + pub type MaxPoolMembersPerPool = StorageValue<_, u32, OptionQuery>; + + /// The maximum commission that can be charged by a pool. Used on commission payouts to bound + /// pool commissions that are > `GlobalMaxCommission`, necessary if a future + /// `GlobalMaxCommission` is lower than some current pool commissions. + #[pallet::storage] + pub type GlobalMaxCommission = StorageValue<_, Perbill, OptionQuery>; + + /// Active members. + /// + /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. + #[pallet::storage] + pub type PoolMembers = + CountedStorageMap<_, Twox64Concat, T::AccountId, PoolMember>; + + /// Storage for bonded pools. + // To get or insert a pool see [`BondedPool::get`] and [`BondedPool::put`] + #[pallet::storage] + pub type BondedPools = + CountedStorageMap<_, Twox64Concat, PoolId, BondedPoolInner>; + + /// Reward pools. This is where there rewards for each pool accumulate. When a members payout is + /// claimed, the balance comes out fo the reward pool. Keyed by the bonded pools account. + #[pallet::storage] + pub type RewardPools = CountedStorageMap<_, Twox64Concat, PoolId, RewardPool>; + + /// Groups of unbonding pools. Each group of unbonding pools belongs to a + /// bonded pool, hence the name sub-pools. Keyed by the bonded pools account. + #[pallet::storage] + pub type SubPoolsStorage = CountedStorageMap<_, Twox64Concat, PoolId, SubPools>; + + /// Metadata for the pool. + #[pallet::storage] + pub type Metadata = + CountedStorageMap<_, Twox64Concat, PoolId, BoundedVec, ValueQuery>; + + /// Ever increasing number of all pools created so far. + #[pallet::storage] + pub type LastPoolId = StorageValue<_, u32, ValueQuery>; + + /// A reverse lookup from the pool's account id to its id. + /// + /// This is only used for slashing. In all other instances, the pool id is used, and the + /// accounts are deterministically derived from it. + #[pallet::storage] + pub type ReversePoolIdLookup = + CountedStorageMap<_, Twox64Concat, T::AccountId, PoolId, OptionQuery>; + + /// Map from a pool member account to their opted claim permission. + #[pallet::storage] + pub type ClaimPermissions = + StorageMap<_, Twox64Concat, T::AccountId, ClaimPermission, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub min_join_bond: BalanceOf, + pub min_create_bond: BalanceOf, + pub max_pools: Option, + pub max_members_per_pool: Option, + pub max_members: Option, + pub global_max_commission: Option, + } + + impl Default for GenesisConfig { + fn default() -> Self { + Self { + min_join_bond: Zero::zero(), + min_create_bond: Zero::zero(), + max_pools: Some(16), + max_members_per_pool: Some(32), + max_members: Some(16 * 32), + global_max_commission: None, + } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + MinJoinBond::::put(self.min_join_bond); + MinCreateBond::::put(self.min_create_bond); + if let Some(max_pools) = self.max_pools { + MaxPools::::put(max_pools); + } + if let Some(max_members_per_pool) = self.max_members_per_pool { + MaxPoolMembersPerPool::::put(max_members_per_pool); + } + if let Some(max_members) = self.max_members { + MaxPoolMembers::::put(max_members); + } + if let Some(global_max_commission) = self.global_max_commission { + GlobalMaxCommission::::put(global_max_commission); + } + } + } + + /// Events of this pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// A pool has been created. + Created { depositor: T::AccountId, pool_id: PoolId }, + /// A member has became bonded in a pool. + Bonded { member: T::AccountId, pool_id: PoolId, bonded: BalanceOf, joined: bool }, + /// A payout has been made to a member. + PaidOut { member: T::AccountId, pool_id: PoolId, payout: BalanceOf }, + /// A member has unbonded from their pool. + /// + /// - `balance` is the corresponding balance of the number of points that has been + /// requested to be unbonded (the argument of the `unbond` transaction) from the bonded + /// pool. + /// - `points` is the number of points that are issued as a result of `balance` being + /// dissolved into the corresponding unbonding pool. + /// - `era` is the era in which the balance will be unbonded. + /// In the absence of slashing, these values will match. In the presence of slashing, the + /// number of points that are issued in the unbonding pool will be less than the amount + /// requested to be unbonded. + Unbonded { + member: T::AccountId, + pool_id: PoolId, + balance: BalanceOf, + points: BalanceOf, + era: EraIndex, + }, + /// A member has withdrawn from their pool. + /// + /// The given number of `points` have been dissolved in return of `balance`. + /// + /// Similar to `Unbonded` event, in the absence of slashing, the ratio of point to balance + /// will be 1. + Withdrawn { + member: T::AccountId, + pool_id: PoolId, + balance: BalanceOf, + points: BalanceOf, + }, + /// A pool has been destroyed. + Destroyed { pool_id: PoolId }, + /// The state of a pool has changed + StateChanged { pool_id: PoolId, new_state: PoolState }, + /// A member has been removed from a pool. + /// + /// The removal can be voluntary (withdrawn all unbonded funds) or involuntary (kicked). + MemberRemoved { pool_id: PoolId, member: T::AccountId }, + /// The roles of a pool have been updated to the given new roles. Note that the depositor + /// can never change. + RolesUpdated { + root: Option, + bouncer: Option, + nominator: Option, + }, + /// The active balance of pool `pool_id` has been slashed to `balance`. + PoolSlashed { pool_id: PoolId, balance: BalanceOf }, + /// The unbond pool at `era` of pool `pool_id` has been slashed to `balance`. + UnbondingPoolSlashed { pool_id: PoolId, era: EraIndex, balance: BalanceOf }, + /// A pool's commission setting has been changed. + PoolCommissionUpdated { pool_id: PoolId, current: Option<(Perbill, T::AccountId)> }, + /// A pool's maximum commission setting has been changed. + PoolMaxCommissionUpdated { pool_id: PoolId, max_commission: Perbill }, + /// A pool's commission `change_rate` has been changed. + PoolCommissionChangeRateUpdated { + pool_id: PoolId, + change_rate: CommissionChangeRate>, + }, + /// Pool commission has been claimed. + PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf }, + } + + #[pallet::error] + #[cfg_attr(test, derive(PartialEq))] + pub enum Error { + /// A (bonded) pool id does not exist. + PoolNotFound, + /// An account is not a member. + PoolMemberNotFound, + /// A reward pool does not exist. In all cases this is a system logic error. + RewardPoolNotFound, + /// A sub pool does not exist. + SubPoolsNotFound, + /// An account is already delegating in another pool. An account may only belong to one + /// pool at a time. + AccountBelongsToOtherPool, + /// The member is fully unbonded (and thus cannot access the bonded and reward pool + /// anymore to, for example, collect rewards). + FullyUnbonding, + /// The member cannot unbond further chunks due to reaching the limit. + MaxUnbondingLimit, + /// None of the funds can be withdrawn yet because the bonding duration has not passed. + CannotWithdrawAny, + /// The amount does not meet the minimum bond to either join or create a pool. + /// + /// The depositor can never unbond to a value less than + /// `Pallet::depositor_min_bond`. The caller does not have nominating + /// permissions for the pool. Members can never unbond to a value below `MinJoinBond`. + MinimumBondNotMet, + /// The transaction could not be executed due to overflow risk for the pool. + OverflowRisk, + /// A pool must be in [`PoolState::Destroying`] in order for the depositor to unbond or for + /// other members to be permissionlessly unbonded. + NotDestroying, + /// The caller does not have nominating permissions for the pool. + NotNominator, + /// Either a) the caller cannot make a valid kick or b) the pool is not destroying. + NotKickerOrDestroying, + /// The pool is not open to join + NotOpen, + /// The system is maxed out on pools. + MaxPools, + /// Too many members in the pool or system. + MaxPoolMembers, + /// The pools state cannot be changed. + CanNotChangeState, + /// The caller does not have adequate permissions. + DoesNotHavePermission, + /// Metadata exceeds [`Config::MaxMetadataLen`] + MetadataExceedsMaxLen, + /// Some error occurred that should never happen. This should be reported to the + /// maintainers. + Defensive(DefensiveError), + /// Partial unbonding now allowed permissionlessly. + PartialUnbondNotAllowedPermissionlessly, + /// The pool's max commission cannot be set higher than the existing value. + MaxCommissionRestricted, + /// The supplied commission exceeds the max allowed commission. + CommissionExceedsMaximum, + /// The supplied commission exceeds global maximum commission. + CommissionExceedsGlobalMaximum, + /// Not enough blocks have surpassed since the last commission update. + CommissionChangeThrottled, + /// The submitted changes to commission change rate are not allowed. + CommissionChangeRateNotAllowed, + /// There is no pending commission to claim. + NoPendingCommission, + /// No commission current has been set. + NoCommissionCurrentSet, + /// Pool id currently in use. + PoolIdInUse, + /// Pool id provided is not correct/usable. + InvalidPoolId, + /// Bonding extra is restricted to the exact pending reward amount. + BondExtraRestricted, + } + + #[derive(Encode, Decode, PartialEq, TypeInfo, PalletError, RuntimeDebug)] + pub enum DefensiveError { + /// There isn't enough space in the unbond pool. + NotEnoughSpaceInUnbondPool, + /// A (bonded) pool id does not exist. + PoolNotFound, + /// A reward pool does not exist. In all cases this is a system logic error. + RewardPoolNotFound, + /// A sub pool does not exist. + SubPoolsNotFound, + /// The bonded account should only be killed by the staking system when the depositor is + /// withdrawing + BondedStashKilledPrematurely, + } + + impl From for Error { + fn from(e: DefensiveError) -> Error { + Error::::Defensive(e) + } + } + + #[pallet::call] + impl Pallet { + /// Stake funds with a pool. The amount to bond is transferred from the member to the + /// pools account and immediately increases the pools bond. + /// + /// # Note + /// + /// * An account can only be a member of a single pool. + /// * An account cannot join the same pool multiple times. + /// * This call will *not* dust the member account, so the member must have at least + /// `existential deposit + amount` in their account. + /// * Only a pool with [`PoolState::Open`] can be joined + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::join())] + pub fn join( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + pool_id: PoolId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(amount >= MinJoinBond::::get(), Error::::MinimumBondNotMet); + // If a member already exists that means they already belong to a pool + ensure!(!PoolMembers::::contains_key(&who), Error::::AccountBelongsToOtherPool); + + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + bonded_pool.ok_to_join()?; + + let mut reward_pool = RewardPools::::get(pool_id) + .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; + // IMPORTANT: reward pool records must be updated with the old points. + reward_pool.update_records( + pool_id, + bonded_pool.points, + bonded_pool.commission.current(), + )?; + + bonded_pool.try_inc_members()?; + let points_issued = bonded_pool.try_bond_funds(&who, amount, BondType::Later)?; + + PoolMembers::insert( + who.clone(), + PoolMember:: { + pool_id, + points: points_issued, + // we just updated `last_known_reward_counter` to the current one in + // `update_recorded`. + last_recorded_reward_counter: reward_pool.last_recorded_reward_counter(), + unbonding_eras: Default::default(), + }, + ); + + Self::deposit_event(Event::::Bonded { + member: who, + pool_id, + bonded: amount, + joined: true, + }); + + bonded_pool.put(); + RewardPools::::insert(pool_id, reward_pool); + + Ok(()) + } + + /// Bond `extra` more funds from `origin` into the pool to which they already belong. + /// + /// Additional funds can come from either the free balance of the account, of from the + /// accumulated rewards, see [`BondExtra`]. + /// + /// Bonding extra funds implies an automatic payout of all pending rewards as well. + /// See `bond_extra_other` to bond pending rewards of `other` members. + // NOTE: this transaction is implemented with the sole purpose of readability and + // correctness, not optimization. We read/write several storage items multiple times instead + // of just once, in the spirit reusing code. + #[pallet::call_index(1)] + #[pallet::weight( + T::WeightInfo::bond_extra_transfer() + .max(T::WeightInfo::bond_extra_other()) + )] + pub fn bond_extra(origin: OriginFor, extra: BondExtra>) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_bond_extra(who.clone(), who, extra) + } + + /// A bonded member can use this to claim their payout based on the rewards that the pool + /// has accumulated since their last claimed payout (OR since joining if this is their first + /// time claiming rewards). The payout will be transferred to the member's account. + /// + /// The member will earn rewards pro rata based on the members stake vs the sum of the + /// members in the pools stake. Rewards do not "expire". + /// + /// See `claim_payout_other` to caim rewards on bahalf of some `other` pool member. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::claim_payout())] + pub fn claim_payout(origin: OriginFor) -> DispatchResult { + let signer = ensure_signed(origin)?; + Self::do_claim_payout(signer.clone(), signer) + } + + /// Unbond up to `unbonding_points` of the `member_account`'s funds from the pool. It + /// implicitly collects the rewards one last time, since not doing so would mean some + /// rewards would be forfeited. + /// + /// Under certain conditions, this call can be dispatched permissionlessly (i.e. by any + /// account). + /// + /// # Conditions for a permissionless dispatch. + /// + /// * The pool is blocked and the caller is either the root or bouncer. This is refereed to + /// as a kick. + /// * The pool is destroying and the member is not the depositor. + /// * The pool is destroying, the member is the depositor and no other members are in the + /// pool. + /// + /// ## Conditions for permissioned dispatch (i.e. the caller is also the + /// `member_account`): + /// + /// * The caller is not the depositor. + /// * The caller is the depositor, the pool is destroying and no other members are in the + /// pool. + /// + /// # Note + /// + /// If there are too many unlocking chunks to unbond with the pool account, + /// [`Call::pool_withdraw_unbonded`] can be called to try and minimize unlocking chunks. + /// The [`StakingInterface::unbond`] will implicitly call [`Call::pool_withdraw_unbonded`] + /// to try to free chunks if necessary (ie. if unbound was called and no unlocking chunks + /// are available). However, it may not be possible to release the current unlocking chunks, + /// in which case, the result of this call will likely be the `NoMoreChunks` error from the + /// staking system. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::unbond())] + pub fn unbond( + origin: OriginFor, + member_account: AccountIdLookupOf, + #[pallet::compact] unbonding_points: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let member_account = T::Lookup::lookup(member_account)?; + let (mut member, mut bonded_pool, mut reward_pool) = + Self::get_member_with_pools(&member_account)?; + + bonded_pool.ok_to_unbond_with(&who, &member_account, &member, unbonding_points)?; + + // Claim the the payout prior to unbonding. Once the user is unbonding their points no + // longer exist in the bonded pool and thus they can no longer claim their payouts. It + // is not strictly necessary to claim the rewards, but we do it here for UX. + reward_pool.update_records( + bonded_pool.id, + bonded_pool.points, + bonded_pool.commission.current(), + )?; + let _ = Self::do_reward_payout(&who, &mut member, &mut bonded_pool, &mut reward_pool)?; + + let current_era = T::Staking::current_era(); + let unbond_era = T::Staking::bonding_duration().saturating_add(current_era); + + // Unbond in the actual underlying nominator. + let unbonding_balance = bonded_pool.dissolve(unbonding_points); + T::Staking::unbond(&bonded_pool.bonded_account(), unbonding_balance)?; + + // Note that we lazily create the unbonding pools here if they don't already exist + let mut sub_pools = SubPoolsStorage::::get(member.pool_id) + .unwrap_or_default() + .maybe_merge_pools(current_era); + + // Update the unbond pool associated with the current era with the unbonded funds. Note + // that we lazily create the unbond pool if it does not yet exist. + if !sub_pools.with_era.contains_key(&unbond_era) { + sub_pools + .with_era + .try_insert(unbond_era, UnbondPool::default()) + // The above call to `maybe_merge_pools` should ensure there is + // always enough space to insert. + .defensive_map_err::, _>(|_| { + DefensiveError::NotEnoughSpaceInUnbondPool.into() + })?; + } + + let points_unbonded = sub_pools + .with_era + .get_mut(&unbond_era) + // The above check ensures the pool exists. + .defensive_ok_or::>(DefensiveError::PoolNotFound.into())? + .issue(unbonding_balance); + + // Try and unbond in the member map. + member.try_unbond(unbonding_points, points_unbonded, unbond_era)?; + + Self::deposit_event(Event::::Unbonded { + member: member_account.clone(), + pool_id: member.pool_id, + points: points_unbonded, + balance: unbonding_balance, + era: unbond_era, + }); + + // Now that we know everything has worked write the items to storage. + SubPoolsStorage::insert(member.pool_id, sub_pools); + Self::put_member_with_pools(&member_account, member, bonded_pool, reward_pool); + Ok(()) + } + + /// Call `withdraw_unbonded` for the pools account. This call can be made by any account. + /// + /// This is useful if their are too many unlocking chunks to call `unbond`, and some + /// can be cleared by withdrawing. In the case there are too many unlocking chunks, the user + /// would probably see an error like `NoMoreChunks` emitted from the staking system when + /// they attempt to unbond. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::pool_withdraw_unbonded(*num_slashing_spans))] + pub fn pool_withdraw_unbonded( + origin: OriginFor, + pool_id: PoolId, + num_slashing_spans: u32, + ) -> DispatchResult { + let _ = ensure_signed(origin)?; + let pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + // For now we only allow a pool to withdraw unbonded if its not destroying. If the pool + // is destroying then `withdraw_unbonded` can be used. + ensure!(pool.state != PoolState::Destroying, Error::::NotDestroying); + T::Staking::withdraw_unbonded(pool.bonded_account(), num_slashing_spans)?; + Ok(()) + } + + /// Withdraw unbonded funds from `member_account`. If no bonded funds can be unbonded, an + /// error is returned. + /// + /// Under certain conditions, this call can be dispatched permissionlessly (i.e. by any + /// account). + /// + /// # Conditions for a permissionless dispatch + /// + /// * The pool is in destroy mode and the target is not the depositor. + /// * The target is the depositor and they are the only member in the sub pools. + /// * The pool is blocked and the caller is either the root or bouncer. + /// + /// # Conditions for permissioned dispatch + /// + /// * The caller is the target and they are not the depositor. + /// + /// # Note + /// + /// If the target is the depositor, the pool will be destroyed. + #[pallet::call_index(5)] + #[pallet::weight( + T::WeightInfo::withdraw_unbonded_kill(*num_slashing_spans) + )] + pub fn withdraw_unbonded( + origin: OriginFor, + member_account: AccountIdLookupOf, + num_slashing_spans: u32, + ) -> DispatchResultWithPostInfo { + let caller = ensure_signed(origin)?; + let member_account = T::Lookup::lookup(member_account)?; + let mut member = + PoolMembers::::get(&member_account).ok_or(Error::::PoolMemberNotFound)?; + let current_era = T::Staking::current_era(); + + let bonded_pool = BondedPool::::get(member.pool_id) + .defensive_ok_or::>(DefensiveError::PoolNotFound.into())?; + let mut sub_pools = + SubPoolsStorage::::get(member.pool_id).ok_or(Error::::SubPoolsNotFound)?; + + bonded_pool.ok_to_withdraw_unbonded_with(&caller, &member_account)?; + + // NOTE: must do this after we have done the `ok_to_withdraw_unbonded_other_with` check. + let withdrawn_points = member.withdraw_unlocked(current_era); + ensure!(!withdrawn_points.is_empty(), Error::::CannotWithdrawAny); + + // Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the + // `transferrable_balance` is correct. + let stash_killed = + T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?; + + // defensive-only: the depositor puts enough funds into the stash so that it will only + // be destroyed when they are leaving. + ensure!( + !stash_killed || caller == bonded_pool.roles.depositor, + Error::::Defensive(DefensiveError::BondedStashKilledPrematurely) + ); + + let mut sum_unlocked_points: BalanceOf = Zero::zero(); + let balance_to_unbond = withdrawn_points + .iter() + .fold(BalanceOf::::zero(), |accumulator, (era, unlocked_points)| { + sum_unlocked_points = sum_unlocked_points.saturating_add(*unlocked_points); + if let Some(era_pool) = sub_pools.with_era.get_mut(era) { + let balance_to_unbond = era_pool.dissolve(*unlocked_points); + if era_pool.points.is_zero() { + sub_pools.with_era.remove(era); + } + accumulator.saturating_add(balance_to_unbond) + } else { + // A pool does not belong to this era, so it must have been merged to the + // era-less pool. + accumulator.saturating_add(sub_pools.no_era.dissolve(*unlocked_points)) + } + }) + // A call to this transaction may cause the pool's stash to get dusted. If this + // happens before the last member has withdrawn, then all subsequent withdraws will + // be 0. However the unbond pools do no get updated to reflect this. In the + // aforementioned scenario, this check ensures we don't try to withdraw funds that + // don't exist. This check is also defensive in cases where the unbond pool does not + // update its balance (e.g. a bug in the slashing hook.) We gracefully proceed in + // order to ensure members can leave the pool and it can be destroyed. + .min(bonded_pool.transferrable_balance()); + + T::Currency::transfer( + &bonded_pool.bonded_account(), + &member_account, + balance_to_unbond, + ExistenceRequirement::AllowDeath, + ) + .defensive()?; + + Self::deposit_event(Event::::Withdrawn { + member: member_account.clone(), + pool_id: member.pool_id, + points: sum_unlocked_points, + balance: balance_to_unbond, + }); + + let post_info_weight = if member.total_points().is_zero() { + // remove any `ClaimPermission` associated with the member. + ClaimPermissions::::remove(&member_account); + + // member being reaped. + PoolMembers::::remove(&member_account); + Self::deposit_event(Event::::MemberRemoved { + pool_id: member.pool_id, + member: member_account.clone(), + }); + + if member_account == bonded_pool.roles.depositor { + Pallet::::dissolve_pool(bonded_pool); + None + } else { + bonded_pool.dec_members().put(); + SubPoolsStorage::::insert(member.pool_id, sub_pools); + Some(T::WeightInfo::withdraw_unbonded_update(num_slashing_spans)) + } + } else { + // we certainly don't need to delete any pools, because no one is being removed. + SubPoolsStorage::::insert(member.pool_id, sub_pools); + PoolMembers::::insert(&member_account, member); + Some(T::WeightInfo::withdraw_unbonded_update(num_slashing_spans)) + }; + + Ok(post_info_weight.into()) + } + + /// Create a new delegation pool. + /// + /// # Arguments + /// + /// * `amount` - The amount of funds to delegate to the pool. This also acts of a sort of + /// deposit since the pools creator cannot fully unbond funds until the pool is being + /// destroyed. + /// * `index` - A disambiguation index for creating the account. Likely only useful when + /// creating multiple pools in the same extrinsic. + /// * `root` - The account to set as [`PoolRoles::root`]. + /// * `nominator` - The account to set as the [`PoolRoles::nominator`]. + /// * `bouncer` - The account to set as the [`PoolRoles::bouncer`]. + /// + /// # Note + /// + /// In addition to `amount`, the caller will transfer the existential deposit; so the caller + /// needs at have at least `amount + existential_deposit` transferrable. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::create())] + pub fn create( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + root: AccountIdLookupOf, + nominator: AccountIdLookupOf, + bouncer: AccountIdLookupOf, + ) -> DispatchResult { + let depositor = ensure_signed(origin)?; + + let pool_id = LastPoolId::::try_mutate::<_, Error, _>(|id| { + *id = id.checked_add(1).ok_or(Error::::OverflowRisk)?; + Ok(*id) + })?; + + Self::do_create(depositor, amount, root, nominator, bouncer, pool_id) + } + + /// Create a new delegation pool with a previously used pool id + /// + /// # Arguments + /// + /// same as `create` with the inclusion of + /// * `pool_id` - `A valid PoolId. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::create())] + pub fn create_with_pool_id( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + root: AccountIdLookupOf, + nominator: AccountIdLookupOf, + bouncer: AccountIdLookupOf, + pool_id: PoolId, + ) -> DispatchResult { + let depositor = ensure_signed(origin)?; + + ensure!(!BondedPools::::contains_key(pool_id), Error::::PoolIdInUse); + ensure!(pool_id < LastPoolId::::get(), Error::::InvalidPoolId); + + Self::do_create(depositor, amount, root, nominator, bouncer, pool_id) + } + + /// Nominate on behalf of the pool. + /// + /// The dispatch origin of this call must be signed by the pool nominator or the pool + /// root role. + /// + /// This directly forward the call to the staking pallet, on behalf of the pool bonded + /// account. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::nominate(validators.len() as u32))] + pub fn nominate( + origin: OriginFor, + pool_id: PoolId, + validators: Vec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_nominate(&who), Error::::NotNominator); + T::Staking::nominate(&bonded_pool.bonded_account(), validators) + } + + /// Set a new state for the pool. + /// + /// If a pool is already in the `Destroying` state, then under no condition can its state + /// change again. + /// + /// The dispatch origin of this call must be either: + /// + /// 1. signed by the bouncer, or the root role of the pool, + /// 2. if the pool conditions to be open are NOT met (as described by `ok_to_be_open`), and + /// then the state of the pool can be permissionlessly changed to `Destroying`. + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::set_state())] + pub fn set_state( + origin: OriginFor, + pool_id: PoolId, + state: PoolState, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.state != PoolState::Destroying, Error::::CanNotChangeState); + + if bonded_pool.can_toggle_state(&who) { + bonded_pool.set_state(state); + } else if bonded_pool.ok_to_be_open().is_err() && state == PoolState::Destroying { + // If the pool has bad properties, then anyone can set it as destroying + bonded_pool.set_state(PoolState::Destroying); + } else { + Err(Error::::CanNotChangeState)?; + } + + bonded_pool.put(); + + Ok(()) + } + + /// Set a new metadata for the pool. + /// + /// The dispatch origin of this call must be signed by the bouncer, or the root role of the + /// pool. + #[pallet::call_index(10)] + #[pallet::weight(T::WeightInfo::set_metadata(metadata.len() as u32))] + pub fn set_metadata( + origin: OriginFor, + pool_id: PoolId, + metadata: Vec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let metadata: BoundedVec<_, _> = + metadata.try_into().map_err(|_| Error::::MetadataExceedsMaxLen)?; + ensure!( + BondedPool::::get(pool_id) + .ok_or(Error::::PoolNotFound)? + .can_set_metadata(&who), + Error::::DoesNotHavePermission + ); + + Metadata::::mutate(pool_id, |pool_meta| *pool_meta = metadata); + + Ok(()) + } + + /// Update configurations for the nomination pools. The origin for this call must be + /// Root. + /// + /// # Arguments + /// + /// * `min_join_bond` - Set [`MinJoinBond`]. + /// * `min_create_bond` - Set [`MinCreateBond`]. + /// * `max_pools` - Set [`MaxPools`]. + /// * `max_members` - Set [`MaxPoolMembers`]. + /// * `max_members_per_pool` - Set [`MaxPoolMembersPerPool`]. + /// * `global_max_commission` - Set [`GlobalMaxCommission`]. + #[pallet::call_index(11)] + #[pallet::weight(T::WeightInfo::set_configs())] + pub fn set_configs( + origin: OriginFor, + min_join_bond: ConfigOp>, + min_create_bond: ConfigOp>, + max_pools: ConfigOp, + max_members: ConfigOp, + max_members_per_pool: ConfigOp, + global_max_commission: ConfigOp, + ) -> DispatchResult { + ensure_root(origin)?; + + macro_rules! config_op_exp { + ($storage:ty, $op:ident) => { + match $op { + ConfigOp::Noop => (), + ConfigOp::Set(v) => <$storage>::put(v), + ConfigOp::Remove => <$storage>::kill(), + } + }; + } + + config_op_exp!(MinJoinBond::, min_join_bond); + config_op_exp!(MinCreateBond::, min_create_bond); + config_op_exp!(MaxPools::, max_pools); + config_op_exp!(MaxPoolMembers::, max_members); + config_op_exp!(MaxPoolMembersPerPool::, max_members_per_pool); + config_op_exp!(GlobalMaxCommission::, global_max_commission); + Ok(()) + } + + /// Update the roles of the pool. + /// + /// The root is the only entity that can change any of the roles, including itself, + /// excluding the depositor, who can never change. + /// + /// It emits an event, notifying UIs of the role change. This event is quite relevant to + /// most pool members and they should be informed of changes to pool roles. + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::update_roles())] + pub fn update_roles( + origin: OriginFor, + pool_id: PoolId, + new_root: ConfigOp, + new_nominator: ConfigOp, + new_bouncer: ConfigOp, + ) -> DispatchResult { + let mut bonded_pool = match ensure_root(origin.clone()) { + Ok(()) => BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?, + Err(frame_support::error::BadOrigin) => { + let who = ensure_signed(origin)?; + let bonded_pool = + BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_update_roles(&who), Error::::DoesNotHavePermission); + bonded_pool + }, + }; + + match new_root { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.root = None, + ConfigOp::Set(v) => bonded_pool.roles.root = Some(v), + }; + match new_nominator { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.nominator = None, + ConfigOp::Set(v) => bonded_pool.roles.nominator = Some(v), + }; + match new_bouncer { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.bouncer = None, + ConfigOp::Set(v) => bonded_pool.roles.bouncer = Some(v), + }; + + Self::deposit_event(Event::::RolesUpdated { + root: bonded_pool.roles.root.clone(), + nominator: bonded_pool.roles.nominator.clone(), + bouncer: bonded_pool.roles.bouncer.clone(), + }); + + bonded_pool.put(); + Ok(()) + } + + /// Chill on behalf of the pool. + /// + /// The dispatch origin of this call must be signed by the pool nominator or the pool + /// root role, same as [`Pallet::nominate`]. + /// + /// This directly forward the call to the staking pallet, on behalf of the pool bonded + /// account. + #[pallet::call_index(13)] + #[pallet::weight(T::WeightInfo::chill())] + pub fn chill(origin: OriginFor, pool_id: PoolId) -> DispatchResult { + let who = ensure_signed(origin)?; + let bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_nominate(&who), Error::::NotNominator); + T::Staking::chill(&bonded_pool.bonded_account()) + } + + /// `origin` bonds funds from `extra` for some pool member `member` into their respective + /// pools. + /// + /// `origin` can bond extra funds from free balance or pending rewards when `origin == + /// other`. + /// + /// In the case of `origin != other`, `origin` can only bond extra pending rewards of + /// `other` members assuming set_claim_permission for the given member is + /// `PermissionlessAll` or `PermissionlessCompound`. + #[pallet::call_index(14)] + #[pallet::weight( + T::WeightInfo::bond_extra_transfer() + .max(T::WeightInfo::bond_extra_other()) + )] + pub fn bond_extra_other( + origin: OriginFor, + member: AccountIdLookupOf, + extra: BondExtra>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_bond_extra(who, T::Lookup::lookup(member)?, extra) + } + + /// Allows a pool member to set a claim permission to allow or disallow permissionless + /// bonding and withdrawing. + /// + /// By default, this is `Permissioned`, which implies only the pool member themselves can + /// claim their pending rewards. If a pool member wishes so, they can set this to + /// `PermissionlessAll` to allow any account to claim their rewards and bond extra to the + /// pool. + /// + /// # Arguments + /// + /// * `origin` - Member of a pool. + /// * `actor` - Account to claim reward. // improve this + #[pallet::call_index(15)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn set_claim_permission( + origin: OriginFor, + permission: ClaimPermission, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(PoolMembers::::contains_key(&who), Error::::PoolMemberNotFound); + ClaimPermissions::::mutate(who, |source| { + *source = permission; + }); + Ok(()) + } + + /// `origin` can claim payouts on some pool member `other`'s behalf. + /// + /// Pool member `other` must have a `PermissionlessAll` or `PermissionlessWithdraw` in order + /// for this call to be successful. + #[pallet::call_index(16)] + #[pallet::weight(T::WeightInfo::claim_payout())] + pub fn claim_payout_other(origin: OriginFor, other: T::AccountId) -> DispatchResult { + let signer = ensure_signed(origin)?; + Self::do_claim_payout(signer, other) + } + + /// Set the commission of a pool. + // + /// Both a commission percentage and a commission payee must be provided in the `current` + /// tuple. Where a `current` of `None` is provided, any current commission will be removed. + /// + /// - If a `None` is supplied to `new_commission`, existing commission will be removed. + #[pallet::call_index(17)] + #[pallet::weight(T::WeightInfo::set_commission())] + pub fn set_commission( + origin: OriginFor, + pool_id: PoolId, + new_commission: Option<(Perbill, T::AccountId)>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + let mut reward_pool = RewardPools::::get(pool_id) + .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; + // IMPORTANT: make sure that everything up to this point is using the current commission + // before it updates. Note that `try_update_current` could still fail at this point. + reward_pool.update_records( + pool_id, + bonded_pool.points, + bonded_pool.commission.current(), + )?; + RewardPools::insert(pool_id, reward_pool); + + bonded_pool.commission.try_update_current(&new_commission)?; + bonded_pool.put(); + Self::deposit_event(Event::::PoolCommissionUpdated { + pool_id, + current: new_commission, + }); + Ok(()) + } + + /// Set the maximum commission of a pool. + /// + /// - Initial max can be set to any `Perbill`, and only smaller values thereafter. + /// - Current commission will be lowered in the event it is higher than a new max + /// commission. + #[pallet::call_index(18)] + #[pallet::weight(T::WeightInfo::set_commission_max())] + pub fn set_commission_max( + origin: OriginFor, + pool_id: PoolId, + max_commission: Perbill, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + bonded_pool.commission.try_update_max(pool_id, max_commission)?; + bonded_pool.put(); + + Self::deposit_event(Event::::PoolMaxCommissionUpdated { pool_id, max_commission }); + Ok(()) + } + + /// Set the commission change rate for a pool. + /// + /// Initial change rate is not bounded, whereas subsequent updates can only be more + /// restrictive than the current. + #[pallet::call_index(19)] + #[pallet::weight(T::WeightInfo::set_commission_change_rate())] + pub fn set_commission_change_rate( + origin: OriginFor, + pool_id: PoolId, + change_rate: CommissionChangeRate>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + bonded_pool.commission.try_update_change_rate(change_rate)?; + bonded_pool.put(); + + Self::deposit_event(Event::::PoolCommissionChangeRateUpdated { + pool_id, + change_rate, + }); + Ok(()) + } + + /// Claim pending commission. + /// + /// The dispatch origin of this call must be signed by the `root` role of the pool. Pending + /// commission is paid out and added to total claimed commission`. Total pending commission + /// is reset to zero. the current. + #[pallet::call_index(20)] + #[pallet::weight(T::WeightInfo::claim_commission())] + pub fn claim_commission(origin: OriginFor, pool_id: PoolId) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_claim_commission(who, pool_id) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> { + Self::do_try_state(u8::MAX) + } + + fn integrity_test() { + assert!( + T::MaxPointsToBalance::get() > 0, + "Minimum points to balance ratio must be greater than 0" + ); + assert!( + T::Staking::bonding_duration() < TotalUnbondingPools::::get(), + "There must be more unbonding pools then the bonding duration / + so a slash can be applied to relevant unboding pools. (We assume / + the bonding duration > slash deffer duration.", + ); + } + } +} + +impl Pallet { + /// The amount of bond that MUST REMAIN IN BONDED in ALL POOLS. + /// + /// It is the responsibility of the depositor to put these funds into the pool initially. Upon + /// unbond, they can never unbond to a value below this amount. + /// + /// 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. + pub fn depositor_min_bond() -> BalanceOf { + T::Staking::minimum_nominator_bond() + .max(MinCreateBond::::get()) + .max(MinJoinBond::::get()) + .max(T::Currency::minimum_balance()) + } + /// Remove everything related to the given bonded pool. + /// + /// Metadata and all of the sub-pools are also deleted. All accounts are dusted and the leftover + /// of the reward account is returned to the depositor. + pub fn dissolve_pool(bonded_pool: BondedPool) { + let reward_account = bonded_pool.reward_account(); + let bonded_account = bonded_pool.bonded_account(); + + ReversePoolIdLookup::::remove(&bonded_account); + RewardPools::::remove(bonded_pool.id); + SubPoolsStorage::::remove(bonded_pool.id); + + // Kill accounts from storage by making their balance go below ED. We assume that the + // accounts have no references that would prevent destruction once we get to this point. We + // don't work with the system pallet directly, but + // 1. we drain the reward account and kill it. This account should never have any extra + // consumers anyway. + // 2. the bonded account should become a 'killed stash' in the staking system, and all of + // its consumers removed. + debug_assert_eq!(frame_system::Pallet::::consumers(&reward_account), 0); + debug_assert_eq!(frame_system::Pallet::::consumers(&bonded_account), 0); + debug_assert_eq!( + T::Staking::total_stake(&bonded_account).unwrap_or_default(), + Zero::zero() + ); + + // This shouldn't fail, but if it does we don't really care. Remaining balance can consist + // of unclaimed pending commission, errorneous transfers to the reward account, etc. + let reward_pool_remaining = T::Currency::free_balance(&reward_account); + let _ = T::Currency::transfer( + &reward_account, + &bonded_pool.roles.depositor, + reward_pool_remaining, + ExistenceRequirement::AllowDeath, + ); + + // NOTE: this is purely defensive. + T::Currency::make_free_balance_be(&reward_account, Zero::zero()); + T::Currency::make_free_balance_be(&bonded_pool.bonded_account(), Zero::zero()); + + Self::deposit_event(Event::::Destroyed { pool_id: bonded_pool.id }); + // Remove bonded pool metadata. + Metadata::::remove(bonded_pool.id); + + bonded_pool.remove(); + } + + /// Create the main, bonded account of a pool with the given id. + pub fn create_bonded_account(id: PoolId) -> T::AccountId { + T::PalletId::get().into_sub_account_truncating((AccountType::Bonded, id)) + } + + /// Create the reward account of a pool with the given id. + pub fn create_reward_account(id: PoolId) -> T::AccountId { + // NOTE: in order to have a distinction in the test account id type (u128), we put + // account_type first so it does not get truncated out. + T::PalletId::get().into_sub_account_truncating((AccountType::Reward, id)) + } + + /// Get the member with their associated bonded and reward pool. + fn get_member_with_pools( + who: &T::AccountId, + ) -> Result<(PoolMember, BondedPool, RewardPool), Error> { + let member = PoolMembers::::get(who).ok_or(Error::::PoolMemberNotFound)?; + let bonded_pool = + BondedPool::::get(member.pool_id).defensive_ok_or(DefensiveError::PoolNotFound)?; + let reward_pool = + RewardPools::::get(member.pool_id).defensive_ok_or(DefensiveError::PoolNotFound)?; + Ok((member, bonded_pool, reward_pool)) + } + + /// Persist the member with their associated bonded and reward pool into storage, consuming + /// all of them. + fn put_member_with_pools( + member_account: &T::AccountId, + member: PoolMember, + bonded_pool: BondedPool, + reward_pool: RewardPool, + ) { + bonded_pool.put(); + RewardPools::insert(member.pool_id, reward_pool); + PoolMembers::::insert(member_account, member); + } + + /// Calculate the equivalent point of `new_funds` in a pool with `current_balance` and + /// `current_points`. + fn balance_to_point( + current_balance: BalanceOf, + current_points: BalanceOf, + new_funds: BalanceOf, + ) -> BalanceOf { + let u256 = T::BalanceToU256::convert; + let balance = T::U256ToBalance::convert; + match (current_balance.is_zero(), current_points.is_zero()) { + (_, true) => new_funds.saturating_mul(POINTS_TO_BALANCE_INIT_RATIO.into()), + (true, false) => { + // The pool was totally slashed. + // This is the equivalent of `(current_points / 1) * new_funds`. + new_funds.saturating_mul(current_points) + }, + (false, false) => { + // Equivalent to (current_points / current_balance) * new_funds + balance( + u256(current_points) + .saturating_mul(u256(new_funds)) + // We check for zero above + .div(u256(current_balance)), + ) + }, + } + } + + /// Calculate the equivalent balance of `points` in a pool with `current_balance` and + /// `current_points`. + fn point_to_balance( + current_balance: BalanceOf, + current_points: BalanceOf, + points: BalanceOf, + ) -> BalanceOf { + let u256 = T::BalanceToU256::convert; + let balance = T::U256ToBalance::convert; + if current_balance.is_zero() || current_points.is_zero() || points.is_zero() { + // There is nothing to unbond + return Zero::zero() + } + + // Equivalent of (current_balance / current_points) * points + balance( + u256(current_balance) + .saturating_mul(u256(points)) + // We check for zero above + .div(u256(current_points)), + ) + } + + /// If the member has some rewards, transfer a payout from the reward pool to the member. + // Emits events and potentially modifies pool state if any arithmetic saturates, but does + // not persist any of the mutable inputs to storage. + fn do_reward_payout( + member_account: &T::AccountId, + member: &mut PoolMember, + bonded_pool: &mut BondedPool, + reward_pool: &mut RewardPool, + ) -> Result, DispatchError> { + debug_assert_eq!(member.pool_id, bonded_pool.id); + + // a member who has no skin in the game anymore cannot claim any rewards. + ensure!(!member.active_points().is_zero(), Error::::FullyUnbonding); + + let (current_reward_counter, _) = reward_pool.current_reward_counter( + bonded_pool.id, + bonded_pool.points, + bonded_pool.commission.current(), + )?; + + // Determine the pending rewards. In scenarios where commission is 100%, `pending_rewards` + // will be zero. + let pending_rewards = member.pending_rewards(current_reward_counter)?; + if pending_rewards.is_zero() { + return Ok(pending_rewards) + } + + // IFF the reward is non-zero alter the member and reward pool info. + member.last_recorded_reward_counter = current_reward_counter; + reward_pool.register_claimed_reward(pending_rewards); + + T::Currency::transfer( + &bonded_pool.reward_account(), + member_account, + pending_rewards, + // defensive: the depositor has put existential deposit into the pool and it stays + // untouched, reward account shall not die. + ExistenceRequirement::KeepAlive, + )?; + + Self::deposit_event(Event::::PaidOut { + member: member_account.clone(), + pool_id: member.pool_id, + payout: pending_rewards, + }); + + Ok(pending_rewards) + } + + fn do_create( + who: T::AccountId, + amount: BalanceOf, + root: AccountIdLookupOf, + nominator: AccountIdLookupOf, + bouncer: AccountIdLookupOf, + pool_id: PoolId, + ) -> DispatchResult { + let root = T::Lookup::lookup(root)?; + let nominator = T::Lookup::lookup(nominator)?; + let bouncer = T::Lookup::lookup(bouncer)?; + + ensure!(amount >= Pallet::::depositor_min_bond(), Error::::MinimumBondNotMet); + ensure!( + MaxPools::::get().map_or(true, |max_pools| BondedPools::::count() < max_pools), + Error::::MaxPools + ); + ensure!(!PoolMembers::::contains_key(&who), Error::::AccountBelongsToOtherPool); + let mut bonded_pool = BondedPool::::new( + pool_id, + PoolRoles { + root: Some(root), + nominator: Some(nominator), + bouncer: Some(bouncer), + depositor: who.clone(), + }, + ); + + bonded_pool.try_inc_members()?; + let points = bonded_pool.try_bond_funds(&who, amount, BondType::Create)?; + + T::Currency::transfer( + &who, + &bonded_pool.reward_account(), + T::Currency::minimum_balance(), + ExistenceRequirement::AllowDeath, + )?; + + PoolMembers::::insert( + who.clone(), + PoolMember:: { + pool_id, + points, + last_recorded_reward_counter: Zero::zero(), + unbonding_eras: Default::default(), + }, + ); + RewardPools::::insert( + pool_id, + RewardPool:: { + last_recorded_reward_counter: Zero::zero(), + last_recorded_total_payouts: Zero::zero(), + total_rewards_claimed: Zero::zero(), + total_commission_pending: Zero::zero(), + total_commission_claimed: Zero::zero(), + }, + ); + ReversePoolIdLookup::::insert(bonded_pool.bonded_account(), pool_id); + + Self::deposit_event(Event::::Created { depositor: who.clone(), pool_id }); + + Self::deposit_event(Event::::Bonded { + member: who, + pool_id, + bonded: amount, + joined: true, + }); + bonded_pool.put(); + + Ok(()) + } + + fn do_bond_extra( + signer: T::AccountId, + who: T::AccountId, + extra: BondExtra>, + ) -> DispatchResult { + if signer != who { + ensure!( + ClaimPermissions::::get(&who).can_bond_extra(), + Error::::DoesNotHavePermission + ); + ensure!(extra == BondExtra::Rewards, Error::::BondExtraRestricted); + } + + let (mut member, mut bonded_pool, mut reward_pool) = Self::get_member_with_pools(&who)?; + + // payout related stuff: we must claim the payouts, and updated recorded payout data + // before updating the bonded pool points, similar to that of `join` transaction. + reward_pool.update_records( + bonded_pool.id, + bonded_pool.points, + bonded_pool.commission.current(), + )?; + let claimed = + Self::do_reward_payout(&who, &mut member, &mut bonded_pool, &mut reward_pool)?; + + let (points_issued, bonded) = match extra { + BondExtra::FreeBalance(amount) => + (bonded_pool.try_bond_funds(&who, amount, BondType::Later)?, amount), + BondExtra::Rewards => + (bonded_pool.try_bond_funds(&who, claimed, BondType::Later)?, claimed), + }; + + bonded_pool.ok_to_be_open()?; + member.points = + member.points.checked_add(&points_issued).ok_or(Error::::OverflowRisk)?; + + Self::deposit_event(Event::::Bonded { + member: who.clone(), + pool_id: member.pool_id, + bonded, + joined: false, + }); + Self::put_member_with_pools(&who, member, bonded_pool, reward_pool); + + Ok(()) + } + + fn do_claim_commission(who: T::AccountId, pool_id: PoolId) -> DispatchResult { + let bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + let mut reward_pool = RewardPools::::get(pool_id) + .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; + + // IMPORTANT: make sure that any newly pending commission not yet processed is added to + // `total_commission_pending`. + reward_pool.update_records( + pool_id, + bonded_pool.points, + bonded_pool.commission.current(), + )?; + + let commission = reward_pool.total_commission_pending; + ensure!(!commission.is_zero(), Error::::NoPendingCommission); + + let payee = bonded_pool + .commission + .current + .as_ref() + .map(|(_, p)| p.clone()) + .ok_or(Error::::NoCommissionCurrentSet)?; + + // Payout claimed commission. + T::Currency::transfer( + &bonded_pool.reward_account(), + &payee, + commission, + ExistenceRequirement::KeepAlive, + )?; + + // Add pending commission to total claimed counter. + reward_pool.total_commission_claimed = + reward_pool.total_commission_claimed.saturating_add(commission); + // Reset total pending commission counter to zero. + reward_pool.total_commission_pending = Zero::zero(); + // Commit reward pool updates + RewardPools::::insert(pool_id, reward_pool); + + Self::deposit_event(Event::::PoolCommissionClaimed { pool_id, commission }); + Ok(()) + } + + fn do_claim_payout(signer: T::AccountId, who: T::AccountId) -> DispatchResult { + if signer != who { + ensure!( + ClaimPermissions::::get(&who).can_claim_payout(), + Error::::DoesNotHavePermission + ); + } + let (mut member, mut bonded_pool, mut reward_pool) = Self::get_member_with_pools(&who)?; + + let _ = Self::do_reward_payout(&who, &mut member, &mut bonded_pool, &mut reward_pool)?; + + Self::put_member_with_pools(&who, member, bonded_pool, reward_pool); + Ok(()) + } + + /// Ensure the correctness of the state of this pallet. + /// + /// This should be valid before or after each state transition of this pallet. + /// + /// ## Invariants: + /// + /// First, let's consider pools: + /// + /// * `BondedPools` and `RewardPools` must all have the EXACT SAME key-set. + /// * `SubPoolsStorage` must be a subset of the above superset. + /// * `Metadata` keys must be a subset of the above superset. + /// * the count of the above set must be less than `MaxPools`. + /// + /// Then, considering members as well: + /// + /// * each `BondedPool.member_counter` must be: + /// - correct (compared to actual count of member who have `.pool_id` this pool) + /// - less than `MaxPoolMembersPerPool`. + /// * each `member.pool_id` must correspond to an existing `BondedPool.id` (which implies the + /// existence of the reward pool as well). + /// * count of all members must be less than `MaxPoolMembers`. + /// + /// Then, considering unbonding members: + /// + /// for each pool: + /// * sum of the balance that's tracked in all unbonding pools must be the same as the + /// unbonded balance of the main account, as reported by the staking interface. + /// * sum of the balance that's tracked in all unbonding pools, plus the bonded balance of the + /// main account should be less than or qual to the total balance of the main account. + /// + /// ## Sanity check level + /// + /// To cater for tests that want to escape parts of these checks, this function is split into + /// multiple `level`s, where the higher the level, the more checks we performs. So, + /// `try_state(255)` is the strongest sanity check, and `0` performs no checks. + #[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))] + pub fn do_try_state(level: u8) -> Result<(), TryRuntimeError> { + if level.is_zero() { + return Ok(()) + } + // note: while a bit wacky, since they have the same key, even collecting to vec should + // result in the same set of keys, in the same order. + let bonded_pools = BondedPools::::iter_keys().collect::>(); + let reward_pools = RewardPools::::iter_keys().collect::>(); + ensure!( + bonded_pools == reward_pools, + "`BondedPools` and `RewardPools` must all have the EXACT SAME key-set." + ); + + ensure!( + SubPoolsStorage::::iter_keys().all(|k| bonded_pools.contains(&k)), + "`SubPoolsStorage` must be a subset of the above superset." + ); + ensure!( + Metadata::::iter_keys().all(|k| bonded_pools.contains(&k)), + "`Metadata` keys must be a subset of the above superset." + ); + + ensure!( + MaxPools::::get().map_or(true, |max| bonded_pools.len() <= (max as usize)), + Error::::MaxPools + ); + + for id in reward_pools { + let account = Self::create_reward_account(id); + if T::Currency::free_balance(&account) < T::Currency::minimum_balance() { + log!( + warn, + "reward pool of {:?}: {:?} (ed = {:?}), should only happen because ED has \ + changed recently. Pool operators should be notified to top up the reward \ + account", + id, + T::Currency::free_balance(&account), + T::Currency::minimum_balance(), + ) + } + } + + let mut pools_members = BTreeMap::::new(); + let mut pools_members_pending_rewards = BTreeMap::>::new(); + let mut all_members = 0u32; + PoolMembers::::iter().try_for_each(|(_, d)| -> Result<(), TryRuntimeError> { + let bonded_pool = BondedPools::::get(d.pool_id).unwrap(); + ensure!(!d.total_points().is_zero(), "No member should have zero points"); + *pools_members.entry(d.pool_id).or_default() += 1; + all_members += 1; + + let reward_pool = RewardPools::::get(d.pool_id).unwrap(); + if !bonded_pool.points.is_zero() { + let commission = bonded_pool.commission.current(); + let (current_rc, _) = reward_pool + .current_reward_counter(d.pool_id, bonded_pool.points, commission) + .unwrap(); + let pending_rewards = d.pending_rewards(current_rc).unwrap(); + *pools_members_pending_rewards.entry(d.pool_id).or_default() += pending_rewards; + } // else this pool has been heavily slashed and cannot have any rewards anymore. + + Ok(()) + })?; + + RewardPools::::iter_keys().try_for_each(|id| -> Result<(), TryRuntimeError> { + // 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. + let pending_rewards_lt_leftover_bal = RewardPool::::current_balance(id) >= + pools_members_pending_rewards.get(&id).copied().unwrap_or_default(); + if !pending_rewards_lt_leftover_bal { + log::warn!( + "pool {:?}, sum pending rewards = {:?}, remaining balance = {:?}", + id, + pools_members_pending_rewards.get(&id), + RewardPool::::current_balance(id) + ); + } + ensure!( + pending_rewards_lt_leftover_bal, + "The sum of the pending rewards must be less than the leftover balance." + ); + Ok(()) + })?; + + BondedPools::::iter().try_for_each(|(id, inner)| -> Result<(), TryRuntimeError> { + let bonded_pool = BondedPool { id, inner }; + ensure!( + pools_members.get(&id).copied().unwrap_or_default() == + bonded_pool.member_counter, + "Each `BondedPool.member_counter` must be equal to the actual count of members of this pool" + ); + ensure!( + MaxPoolMembersPerPool::::get() + .map_or(true, |max| bonded_pool.member_counter <= max), + Error::::MaxPoolMembers + ); + + let depositor = PoolMembers::::get(&bonded_pool.roles.depositor).unwrap(); + ensure!( + bonded_pool.is_destroying_and_only_depositor(depositor.active_points()) || + depositor.active_points() >= MinCreateBond::::get(), + "depositor must always have MinCreateBond stake in the pool, except for when the \ + pool is being destroyed and the depositor is the last member", + ); + Ok(()) + })?; + ensure!( + MaxPoolMembers::::get().map_or(true, |max| all_members <= max), + Error::::MaxPoolMembers + ); + + if level <= 1 { + return Ok(()) + } + + for (pool_id, _pool) in BondedPools::::iter() { + let pool_account = Pallet::::create_bonded_account(pool_id); + let subs = SubPoolsStorage::::get(pool_id).unwrap_or_default(); + + let sum_unbonding_balance = subs.sum_unbonding_balance(); + let bonded_balance = T::Staking::active_stake(&pool_account).unwrap_or_default(); + let total_balance = T::Currency::total_balance(&pool_account); + + assert!( + total_balance >= bonded_balance + sum_unbonding_balance, + "faulty pool: {:?} / {:?}, total_balance {:?} >= bonded_balance {:?} + sum_unbonding_balance {:?}", + pool_id, + _pool, + total_balance, + bonded_balance, + sum_unbonding_balance + ); + } + + Ok(()) + } + + /// Fully unbond the shares of `member`, when executed from `origin`. + /// + /// This is useful for backwards compatibility with the majority of tests that only deal with + /// full unbonding, not partial unbonding. + #[cfg(any(feature = "runtime-benchmarks", test))] + pub fn fully_unbond( + origin: frame_system::pallet_prelude::OriginFor, + member: T::AccountId, + ) -> DispatchResult { + let points = PoolMembers::::get(&member).map(|d| d.active_points()).unwrap_or_default(); + let member_lookup = T::Lookup::unlookup(member); + Self::unbond(origin, member_lookup, points) + } +} + +impl Pallet { + /// Returns the pending rewards for the specified `who` account. + /// + /// In the case of error, `None` is returned. Used by runtime API. + pub fn api_pending_rewards(who: T::AccountId) -> Option> { + if let Some(pool_member) = PoolMembers::::get(who) { + if let Some((reward_pool, bonded_pool)) = RewardPools::::get(pool_member.pool_id) + .zip(BondedPools::::get(pool_member.pool_id)) + { + let commission = bonded_pool.commission.current(); + let (current_reward_counter, _) = reward_pool + .current_reward_counter(pool_member.pool_id, bonded_pool.points, commission) + .ok()?; + return pool_member.pending_rewards(current_reward_counter).ok() + } + } + + None + } + + /// Returns the points to balance conversion for a specified pool. + /// + /// If the pool ID does not exist, it returns 0 ratio points to balance. Used by runtime API. + pub fn api_points_to_balance(pool_id: PoolId, points: BalanceOf) -> BalanceOf { + if let Some(pool) = BondedPool::::get(pool_id) { + pool.points_to_balance(points) + } else { + Zero::zero() + } + } + + /// Returns the equivalent `new_funds` balance to point conversion for a specified pool. + /// + /// If the pool ID does not exist, returns 0 ratio balance to points. Used by runtime API. + pub fn api_balance_to_points(pool_id: PoolId, new_funds: BalanceOf) -> BalanceOf { + if let Some(pool) = BondedPool::::get(pool_id) { + let bonded_balance = + T::Staking::active_stake(&pool.bonded_account()).unwrap_or(Zero::zero()); + Pallet::::balance_to_point(bonded_balance, pool.points, new_funds) + } else { + Zero::zero() + } + } +} + +impl sp_staking::OnStakingUpdate> for Pallet { + fn on_slash( + pool_account: &T::AccountId, + // Bonded balance is always read directly from staking, therefore we don't need to update + // anything here. + slashed_bonded: BalanceOf, + slashed_unlocking: &BTreeMap>, + ) { + if let Some(pool_id) = ReversePoolIdLookup::::get(pool_account) { + let mut sub_pools = match SubPoolsStorage::::get(pool_id).defensive() { + Some(sub_pools) => sub_pools, + None => return, + }; + for (era, slashed_balance) in slashed_unlocking.iter() { + if let Some(pool) = sub_pools.with_era.get_mut(era) { + pool.balance = *slashed_balance; + Self::deposit_event(Event::::UnbondingPoolSlashed { + era: *era, + pool_id, + balance: *slashed_balance, + }); + } + } + + Self::deposit_event(Event::::PoolSlashed { pool_id, balance: slashed_bonded }); + SubPoolsStorage::::insert(pool_id, sub_pools); + } + } +} diff --git a/pallets/nomination-pools/src/migration.rs b/pallets/nomination-pools/src/migration.rs new file mode 100644 index 000000000..2ae4cd1b8 --- /dev/null +++ b/pallets/nomination-pools/src/migration.rs @@ -0,0 +1,727 @@ +// This file is part of Substrate. + +// Copyright (C) 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 super::*; +use crate::log; +use frame_support::traits::OnRuntimeUpgrade; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + +#[cfg(feature = "try-runtime")] +use sp_runtime::TryRuntimeError; + +pub mod v1 { + use super::*; + + #[derive(Decode)] + pub struct OldPoolRoles { + pub depositor: AccountId, + pub root: AccountId, + pub nominator: AccountId, + pub bouncer: AccountId, + } + + impl OldPoolRoles { + fn migrate_to_v1(self) -> PoolRoles { + PoolRoles { + depositor: self.depositor, + root: Some(self.root), + nominator: Some(self.nominator), + bouncer: Some(self.bouncer), + } + } + } + + #[derive(Decode)] + pub struct OldBondedPoolInner { + pub points: BalanceOf, + pub state: PoolState, + pub member_counter: u32, + pub roles: OldPoolRoles, + } + + impl OldBondedPoolInner { + fn migrate_to_v1(self) -> BondedPoolInner { + // Note: `commission` field not introduced to `BondedPoolInner` until + // migration 4. + BondedPoolInner { + points: self.points, + commission: Commission::default(), + member_counter: self.member_counter, + state: self.state, + roles: self.roles.migrate_to_v1(), + } + } + } + + /// Trivial migration which makes the roles of each pool optional. + /// + /// Note: The depositor is not optional since they can never change. + pub struct MigrateToV1(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV1 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + log!( + info, + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + if current == 1 && onchain == 0 { + // this is safe to execute on any runtime that has a bounded number of pools. + let mut translated = 0u64; + BondedPools::::translate::, _>(|_key, old_value| { + translated.saturating_inc(); + Some(old_value.migrate_to_v1()) + }); + + current.put::>(); + + log!(info, "Upgraded {} pools, storage to version {:?}", translated, current); + + T::DbWeight::get().reads_writes(translated + 1, translated + 1) + } else { + log!(info, "Migration did not executed. This probably should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), TryRuntimeError> { + // new version must be set. + ensure!( + Pallet::::on_chain_storage_version() == 1, + "The onchain version must be updated after the migration." + ); + Pallet::::try_state(frame_system::Pallet::::block_number())?; + Ok(()) + } + } +} + +pub mod v2 { + use super::*; + use sp_runtime::Perbill; + + #[test] + fn migration_assumption_is_correct() { + // this migrations cleans all the reward accounts to contain exactly ed, and all members + // having no claimable rewards. In this state, all fields of the `RewardPool` and + // `member.last_recorded_reward_counter` are all zero. + use crate::mock::*; + ExtBuilder::default().build_and_execute(|| { + let join = |x| { + Balances::make_free_balance_be(&x, Balances::minimum_balance() + 10); + frame_support::assert_ok!(Pools::join(RuntimeOrigin::signed(x), 10, 1)); + }; + + assert_eq!(BondedPool::::get(1).unwrap().points, 10); + assert_eq!( + RewardPools::::get(1).unwrap(), + RewardPool { ..Default::default() } + ); + assert_eq!( + PoolMembers::::get(10).unwrap().last_recorded_reward_counter, + Zero::zero() + ); + + join(20); + assert_eq!(BondedPool::::get(1).unwrap().points, 20); + assert_eq!( + RewardPools::::get(1).unwrap(), + RewardPool { ..Default::default() } + ); + assert_eq!( + PoolMembers::::get(10).unwrap().last_recorded_reward_counter, + Zero::zero() + ); + assert_eq!( + PoolMembers::::get(20).unwrap().last_recorded_reward_counter, + Zero::zero() + ); + + join(30); + assert_eq!(BondedPool::::get(1).unwrap().points, 30); + assert_eq!( + RewardPools::::get(1).unwrap(), + RewardPool { ..Default::default() } + ); + assert_eq!( + PoolMembers::::get(10).unwrap().last_recorded_reward_counter, + Zero::zero() + ); + assert_eq!( + PoolMembers::::get(20).unwrap().last_recorded_reward_counter, + Zero::zero() + ); + assert_eq!( + PoolMembers::::get(30).unwrap().last_recorded_reward_counter, + Zero::zero() + ); + }); + } + + #[derive(Decode)] + pub struct OldRewardPool { + pub balance: B, + pub total_earnings: B, + pub points: U256, + } + + #[derive(Decode)] + pub struct OldPoolMember { + pub pool_id: PoolId, + pub points: BalanceOf, + pub reward_pool_total_earnings: BalanceOf, + pub unbonding_eras: BoundedBTreeMap, T::MaxUnbonding>, + } + + /// Migrate the pool reward scheme to the new version, as per + /// . + pub struct MigrateToV2(sp_std::marker::PhantomData); + impl MigrateToV2 { + fn run(current: StorageVersion) -> Weight { + let mut reward_pools_translated = 0u64; + let mut members_translated = 0u64; + // just for logging. + let mut total_value_locked = BalanceOf::::zero(); + let mut total_points_locked = BalanceOf::::zero(); + + // store each member of the pool, with their active points. In the process, migrate + // their data as well. + let mut temp_members = BTreeMap::)>>::new(); + PoolMembers::::translate::, _>(|key, old_member| { + let id = old_member.pool_id; + temp_members.entry(id).or_default().push((key, old_member.points)); + + total_points_locked += old_member.points; + members_translated += 1; + Some(PoolMember:: { + last_recorded_reward_counter: Zero::zero(), + pool_id: old_member.pool_id, + points: old_member.points, + unbonding_eras: old_member.unbonding_eras, + }) + }); + + // translate all reward pools. In the process, do the last payout as well. + RewardPools::::translate::>, _>( + |id, _old_reward_pool| { + // each pool should have at least one member. + let members = match temp_members.get(&id) { + Some(x) => x, + None => { + log!(error, "pool {} has no member! deleting it..", id); + return None + }, + }; + let bonded_pool = match BondedPools::::get(id) { + Some(x) => x, + None => { + log!(error, "pool {} has no bonded pool! deleting it..", id); + return None + }, + }; + + let accumulated_reward = RewardPool::::current_balance(id); + let reward_account = Pallet::::create_reward_account(id); + let mut sum_paid_out = BalanceOf::::zero(); + + members + .into_iter() + .filter_map(|(who, points)| { + let bonded_pool = match BondedPool::::get(id) { + Some(x) => x, + None => { + log!(error, "pool {} for member {:?} does not exist!", id, who); + return None + }, + }; + + total_value_locked += bonded_pool.points_to_balance(*points); + let portion = Perbill::from_rational(*points, bonded_pool.points); + let last_claim = portion * accumulated_reward; + + log!( + debug, + "{:?} has {:?} ({:?}) of pool {} with total reward of {:?}", + who, + portion, + last_claim, + id, + accumulated_reward + ); + + if last_claim.is_zero() { + None + } else { + Some((who, last_claim)) + } + }) + .for_each(|(who, last_claim)| { + let outcome = T::Currency::transfer( + &reward_account, + &who, + last_claim, + ExistenceRequirement::KeepAlive, + ); + + if let Err(reason) = outcome { + log!(warn, "last reward claim failed due to {:?}", reason,); + } else { + sum_paid_out = sum_paid_out.saturating_add(last_claim); + } + + Pallet::::deposit_event(Event::::PaidOut { + member: who.clone(), + pool_id: id, + payout: last_claim, + }); + }); + + // this can only be because of rounding down, or because the person we + // wanted to pay their reward to could not accept it (dust). + let leftover = accumulated_reward.saturating_sub(sum_paid_out); + if !leftover.is_zero() { + // pay it all to depositor. + let o = T::Currency::transfer( + &reward_account, + &bonded_pool.roles.depositor, + leftover, + ExistenceRequirement::KeepAlive, + ); + log!(warn, "paying {:?} leftover to the depositor: {:?}", leftover, o); + } + + // finally, migrate the reward pool. + reward_pools_translated += 1; + + Some(RewardPool { + last_recorded_reward_counter: Zero::zero(), + last_recorded_total_payouts: Zero::zero(), + total_rewards_claimed: Zero::zero(), + total_commission_claimed: Zero::zero(), + total_commission_pending: Zero::zero(), + }) + }, + ); + + log!( + info, + "Upgraded {} members, {} reward pools, TVL {:?} TPL {:?}, storage to version {:?}", + members_translated, + reward_pools_translated, + total_value_locked, + total_points_locked, + current + ); + current.put::>(); + + T::DbWeight::get().reads_writes(members_translated + 1, reward_pools_translated + 1) + } + } + + impl OnRuntimeUpgrade for MigrateToV2 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + log!( + info, + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + if current == 2 && onchain == 1 { + Self::run(current) + } else { + log!(info, "MigrateToV2 did not executed. This probably should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + // all reward accounts must have more than ED. + RewardPools::::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> { + ensure!( + T::Currency::free_balance(&Pallet::::create_reward_account(id)) >= + T::Currency::minimum_balance(), + "Reward accounts must have greater balance than ED." + ); + Ok(()) + })?; + + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), TryRuntimeError> { + // new version must be set. + ensure!( + Pallet::::on_chain_storage_version() == 2, + "The onchain version must be updated after the migration." + ); + + // no reward or bonded pool has been skipped. + ensure!( + RewardPools::::iter().count() as u32 == RewardPools::::count(), + "The count of reward pools must remain the same after the migration." + ); + ensure!( + BondedPools::::iter().count() as u32 == BondedPools::::count(), + "The count of reward pools must remain the same after the migration." + ); + + // all reward pools must have exactly ED in them. This means no reward can be claimed, + // and that setting reward counters all over the board to zero will work henceforth. + RewardPools::::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> { + ensure!( + RewardPool::::current_balance(id) == Zero::zero(), + "Reward pool balance must be zero.", + ); + Ok(()) + })?; + + log!(info, "post upgrade hook for MigrateToV2 executed."); + Ok(()) + } + } +} + +pub mod v3 { + use super::*; + + /// This migration removes stale bonded-pool metadata, if any. + pub struct MigrateToV3(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV3 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + if onchain == 2 { + log!( + info, + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + let mut metadata_iterated = 0u64; + let mut metadata_removed = 0u64; + Metadata::::iter_keys() + .filter(|id| { + metadata_iterated += 1; + !BondedPools::::contains_key(&id) + }) + .collect::>() + .into_iter() + .for_each(|id| { + metadata_removed += 1; + Metadata::::remove(&id); + }); + StorageVersion::new(3).put::>(); + // metadata iterated + bonded pools read + a storage version read + let total_reads = metadata_iterated * 2 + 1; + // metadata removed + a storage version write + let total_writes = metadata_removed + 1; + T::DbWeight::get().reads_writes(total_reads, total_writes) + } else { + log!(info, "MigrateToV3 should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), TryRuntimeError> { + ensure!( + Metadata::::iter_keys().all(|id| BondedPools::::contains_key(&id)), + "not all of the stale metadata has been removed" + ); + ensure!( + Pallet::::on_chain_storage_version() >= 3, + "nomination-pools::migration::v3: wrong storage version" + ); + Ok(()) + } + } +} + +pub mod v4 { + use super::*; + + #[derive(Decode)] + pub struct OldBondedPoolInner { + pub points: BalanceOf, + pub state: PoolState, + pub member_counter: u32, + pub roles: PoolRoles, + } + + impl OldBondedPoolInner { + fn migrate_to_v4(self) -> BondedPoolInner { + BondedPoolInner { + commission: Commission::default(), + member_counter: self.member_counter, + points: self.points, + state: self.state, + roles: self.roles, + } + } + } + + /// Migrates from `v3` directly to `v5` to avoid the broken `v4` migration. + #[allow(deprecated)] + pub type MigrateV3ToV5 = (v4::MigrateToV4, v5::MigrateToV5); + + /// # Warning + /// + /// To avoid mangled storage please use `MigrateV3ToV5` instead. + /// See: github.com/paritytech/substrate/pull/13715 + /// + /// This migration adds a `commission` field to every `BondedPoolInner`, if + /// any. + #[deprecated( + note = "To avoid mangled storage please use `MigrateV3ToV5` instead. See: github.com/paritytech/substrate/pull/13715" + )] + pub struct MigrateToV4(sp_std::marker::PhantomData<(T, U)>); + #[allow(deprecated)] + impl> OnRuntimeUpgrade for MigrateToV4 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + log!( + info, + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + if onchain == 3 { + log!(warn, "Please run MigrateToV5 immediately after this migration. See github.com/paritytech/substrate/pull/13715"); + let initial_global_max_commission = U::get(); + GlobalMaxCommission::::set(Some(initial_global_max_commission)); + log!( + info, + "Set initial global max commission to {:?}.", + initial_global_max_commission + ); + + let mut translated = 0u64; + BondedPools::::translate::, _>(|_key, old_value| { + translated.saturating_inc(); + Some(old_value.migrate_to_v4()) + }); + + StorageVersion::new(4).put::>(); + log!(info, "Upgraded {} pools, storage to version {:?}", translated, current); + + // reads: translated + onchain version. + // writes: translated + current.put + initial global commission. + T::DbWeight::get().reads_writes(translated + 1, translated + 2) + } else { + log!(info, "Migration did not execute. This probably should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), TryRuntimeError> { + // ensure all BondedPools items now contain an `inner.commission: Commission` field. + ensure!( + BondedPools::::iter().all(|(_, inner)| + // Check current + (inner.commission.current.is_none() || + inner.commission.current.is_some()) && + // Check max + (inner.commission.max.is_none() || inner.commission.max.is_some()) && + // Check change_rate + (inner.commission.change_rate.is_none() || + inner.commission.change_rate.is_some()) && + // Check throttle_from + (inner.commission.throttle_from.is_none() || + inner.commission.throttle_from.is_some())), + "a commission value has not been set correctly" + ); + ensure!( + GlobalMaxCommission::::get() == Some(U::get()), + "global maximum commission error" + ); + ensure!( + Pallet::::on_chain_storage_version() >= 4, + "nomination-pools::migration::v4: wrong storage version" + ); + Ok(()) + } + } +} + +pub mod v5 { + use super::*; + + #[derive(Decode)] + pub struct OldRewardPool { + last_recorded_reward_counter: T::RewardCounter, + last_recorded_total_payouts: BalanceOf, + total_rewards_claimed: BalanceOf, + } + + impl OldRewardPool { + fn migrate_to_v5(self) -> RewardPool { + RewardPool { + last_recorded_reward_counter: self.last_recorded_reward_counter, + last_recorded_total_payouts: self.last_recorded_total_payouts, + total_rewards_claimed: self.total_rewards_claimed, + total_commission_pending: Zero::zero(), + total_commission_claimed: Zero::zero(), + } + } + } + + /// This migration adds `total_commission_pending` and `total_commission_claimed` field to every + /// `RewardPool`, if any. + pub struct MigrateToV5(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV5 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + log!( + info, + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + if current == 5 && onchain == 4 { + let mut translated = 0u64; + RewardPools::::translate::, _>(|_id, old_value| { + translated.saturating_inc(); + Some(old_value.migrate_to_v5()) + }); + + current.put::>(); + log!(info, "Upgraded {} pools, storage to version {:?}", translated, current); + + // reads: translated + onchain version. + // writes: translated + current.put. + T::DbWeight::get().reads_writes(translated + 1, translated + 1) + } else { + log!(info, "Migration did not execute. This probably should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + let rpool_keys = RewardPools::::iter_keys().count(); + let rpool_values = RewardPools::::iter_values().count(); + if rpool_keys != rpool_values { + log!(info, "🔥 There are {} undecodable RewardPools in storage. This migration will try to correct them. keys: {}, values: {}", rpool_keys.saturating_sub(rpool_values), rpool_keys, rpool_values); + } + + ensure!( + PoolMembers::::iter_keys().count() == PoolMembers::::iter_values().count(), + "There are undecodable PoolMembers in storage. This migration will not fix that." + ); + ensure!( + BondedPools::::iter_keys().count() == BondedPools::::iter_values().count(), + "There are undecodable BondedPools in storage. This migration will not fix that." + ); + ensure!( + SubPoolsStorage::::iter_keys().count() == + SubPoolsStorage::::iter_values().count(), + "There are undecodable SubPools in storage. This migration will not fix that." + ); + ensure!( + Metadata::::iter_keys().count() == Metadata::::iter_values().count(), + "There are undecodable Metadata in storage. This migration will not fix that." + ); + + Ok((rpool_values as u64).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(data: Vec) -> Result<(), TryRuntimeError> { + let old_rpool_values: u64 = Decode::decode(&mut &data[..]).unwrap(); + let rpool_keys = RewardPools::::iter_keys().count() as u64; + let rpool_values = RewardPools::::iter_values().count() as u64; + ensure!( + rpool_keys == rpool_values, + "There are STILL undecodable RewardPools - migration failed" + ); + + if old_rpool_values != rpool_values { + log!( + info, + "🎉 Fixed {} undecodable RewardPools.", + rpool_values.saturating_sub(old_rpool_values) + ); + } + + // ensure all RewardPools items now contain `total_commission_pending` and + // `total_commission_claimed` field. + ensure!( + RewardPools::::iter().all(|(_, reward_pool)| reward_pool + .total_commission_pending >= + Zero::zero() && reward_pool + .total_commission_claimed >= + Zero::zero()), + "a commission value has been incorrectly set" + ); + ensure!( + Pallet::::on_chain_storage_version() >= 5, + "nomination-pools::migration::v5: wrong storage version" + ); + + // These should not have been touched - just in case. + ensure!( + PoolMembers::::iter_keys().count() == PoolMembers::::iter_values().count(), + "There are undecodable PoolMembers in storage." + ); + ensure!( + BondedPools::::iter_keys().count() == BondedPools::::iter_values().count(), + "There are undecodable BondedPools in storage." + ); + ensure!( + SubPoolsStorage::::iter_keys().count() == + SubPoolsStorage::::iter_values().count(), + "There are undecodable SubPools in storage." + ); + ensure!( + Metadata::::iter_keys().count() == Metadata::::iter_values().count(), + "There are undecodable Metadata in storage." + ); + + Ok(()) + } + } +} diff --git a/pallets/nomination-pools/src/mock.rs b/pallets/nomination-pools/src/mock.rs new file mode 100644 index 000000000..7d0d729a4 --- /dev/null +++ b/pallets/nomination-pools/src/mock.rs @@ -0,0 +1,446 @@ +use super::*; +use crate::{self as pools}; +use frame_support::{assert_ok, parameter_types, PalletId}; +use frame_system::RawOrigin; +use sp_runtime::{BuildStorage, FixedU128}; +use sp_staking::Stake; + +pub type BlockNumber = u64; +pub type AccountId = u128; +pub type Balance = u128; +pub type RewardCounter = FixedU128; +// This sneaky little hack allows us to write code exactly as we would do in the pallet in the tests +// as well, e.g. `StorageItem::::get()`. +pub type T = Runtime; + +// Ext builder creates a pool with id 1. +pub fn default_bonded_account() -> AccountId { + Pools::create_bonded_account(1) +} + +// Ext builder creates a pool with id 1. +pub fn default_reward_account() -> AccountId { + Pools::create_reward_account(1) +} + +parameter_types! { + pub static MinJoinBondConfig: Balance = 2; + pub static CurrentEra: EraIndex = 0; + pub static BondingDuration: EraIndex = 3; + pub storage BondedBalanceMap: BTreeMap = Default::default(); + pub storage UnbondingBalanceMap: BTreeMap = Default::default(); + #[derive(Clone, PartialEq)] + pub static MaxUnbonding: u32 = 8; + pub static StakingMinBond: Balance = 10; + pub storage Nominations: Option> = None; +} + +pub struct StakingMock; +impl StakingMock { + pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) { + let mut x = BondedBalanceMap::get(); + x.insert(who, bonded); + BondedBalanceMap::set(&x) + } +} + +impl sp_staking::StakingInterface for StakingMock { + type Balance = Balance; + type AccountId = AccountId; + type CurrencyToVote = (); + + fn minimum_nominator_bond() -> Self::Balance { + StakingMinBond::get() + } + fn minimum_validator_bond() -> Self::Balance { + StakingMinBond::get() + } + + fn desired_validator_count() -> u32 { + unimplemented!("method currently not used in testing") + } + + fn current_era() -> EraIndex { + CurrentEra::get() + } + + fn bonding_duration() -> EraIndex { + BondingDuration::get() + } + + fn status( + _: &Self::AccountId, + ) -> Result, DispatchError> { + Nominations::get() + .map(|noms| sp_staking::StakerStatus::Nominator(noms)) + .ok_or(DispatchError::Other("NotStash")) + } + + fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { + let mut x = BondedBalanceMap::get(); + x.get_mut(who).map(|v| *v += extra); + BondedBalanceMap::set(&x); + Ok(()) + } + + fn unbond(who: &Self::AccountId, amount: Self::Balance) -> DispatchResult { + let mut x = BondedBalanceMap::get(); + *x.get_mut(who).unwrap() = x.get_mut(who).unwrap().saturating_sub(amount); + BondedBalanceMap::set(&x); + let mut y = UnbondingBalanceMap::get(); + *y.entry(*who).or_insert(Self::Balance::zero()) += amount; + UnbondingBalanceMap::set(&y); + Ok(()) + } + + fn chill(_: &Self::AccountId) -> sp_runtime::DispatchResult { + Ok(()) + } + + fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result { + // Simulates removing unlocking chunks and only having the bonded balance locked + let mut x = UnbondingBalanceMap::get(); + x.remove(&who); + UnbondingBalanceMap::set(&x); + + Ok(UnbondingBalanceMap::get().is_empty() && BondedBalanceMap::get().is_empty()) + } + + fn bond(stash: &Self::AccountId, value: Self::Balance, _: &Self::AccountId) -> DispatchResult { + StakingMock::set_bonded_balance(*stash, value); + Ok(()) + } + + fn nominate(_: &Self::AccountId, nominations: Vec) -> DispatchResult { + Nominations::set(&Some(nominations)); + Ok(()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn nominations(_: &Self::AccountId) -> Option> { + Nominations::get() + } + + fn stash_by_ctrl(_controller: &Self::AccountId) -> Result { + unimplemented!("method currently not used in testing") + } + + fn stake(who: &Self::AccountId) -> Result, DispatchError> { + match ( + UnbondingBalanceMap::get().get(who).copied(), + BondedBalanceMap::get().get(who).copied(), + ) { + (None, None) => Err(DispatchError::Other("balance not found")), + (Some(v), None) => Ok(Stake { total: v, active: 0 }), + (None, Some(v)) => Ok(Stake { total: v, active: v }), + (Some(a), Some(b)) => Ok(Stake { total: a + b, active: b }), + } + } + + fn election_ongoing() -> bool { + unimplemented!("method currently not used in testing") + } + + fn force_unstake(_who: Self::AccountId) -> sp_runtime::DispatchResult { + unimplemented!("method currently not used in testing") + } + + fn is_exposed_in_era(_who: &Self::AccountId, _era: &EraIndex) -> bool { + unimplemented!("method currently not used in testing") + } + + #[cfg(feature = "runtime-benchmarks")] + fn add_era_stakers( + _current_era: &EraIndex, + _stash: &Self::AccountId, + _exposures: Vec<(Self::AccountId, Self::Balance)>, + ) { + unimplemented!("method currently not used in testing") + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_current_era(_era: EraIndex) { + unimplemented!("method currently not used in testing") + } +} + +impl frame_system::Config for Runtime { + type SS58Prefix = (); + type BaseCallFilter = frame_support::traits::Everything; + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = sp_runtime::traits::IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type DbWeight = (); + type BlockLength = (); + type BlockWeights = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub static ExistentialDeposit: Balance = 5; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = frame_support::traits::ConstU32<1024>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type MaxHolds = (); +} + +pub struct BalanceToU256; +impl Convert for BalanceToU256 { + fn convert(n: Balance) -> U256 { + n.into() + } +} + +pub struct U256ToBalance; +impl Convert for U256ToBalance { + fn convert(n: U256) -> Balance { + n.try_into().unwrap() + } +} + +parameter_types! { + pub static PostUnbondingPoolsWindow: u32 = 2; + pub static MaxMetadataLen: u32 = 2; + pub static CheckLevel: u8 = 255; + pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls"); +} +impl pools::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RewardCounter = RewardCounter; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = StakingMock; + type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow; + type PalletId = PoolsPalletId; + type MaxMetadataLen = MaxMetadataLen; + type MaxUnbonding = MaxUnbonding; + type MaxPointsToBalance = frame_support::traits::ConstU8<10>; +} + +type Block = frame_system::mocking::MockBlock; +frame_support::construct_runtime!( + pub struct Runtime + { + System: frame_system::{Pallet, Call, Storage, Event, Config}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Pools: pools::{Pallet, Call, Storage, Event}, + } +); + +pub struct ExtBuilder { + members: Vec<(AccountId, Balance)>, + max_members: Option, + max_members_per_pool: Option, + global_max_commission: Option, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + members: Default::default(), + max_members: Some(4), + max_members_per_pool: Some(3), + global_max_commission: Some(Perbill::from_percent(90)), + } + } +} + +#[cfg_attr(feature = "fuzzing", allow(dead_code))] +impl ExtBuilder { + // Add members to pool 0. + pub fn add_members(mut self, members: Vec<(AccountId, Balance)>) -> Self { + self.members = members; + self + } + + pub fn ed(self, ed: Balance) -> Self { + ExistentialDeposit::set(ed); + self + } + + pub fn min_bond(self, min: Balance) -> Self { + StakingMinBond::set(min); + self + } + + pub fn min_join_bond(self, min: Balance) -> Self { + MinJoinBondConfig::set(min); + self + } + + pub fn with_check(self, level: u8) -> Self { + CheckLevel::set(level); + self + } + + pub fn max_members(mut self, max: Option) -> Self { + self.max_members = max; + self + } + + pub fn max_members_per_pool(mut self, max: Option) -> Self { + self.max_members_per_pool = max; + self + } + + pub fn global_max_commission(mut self, commission: Option) -> Self { + self.global_max_commission = commission; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut storage = + frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let _ = crate::GenesisConfig:: { + min_join_bond: MinJoinBondConfig::get(), + min_create_bond: 2, + max_pools: Some(2), + max_members_per_pool: self.max_members_per_pool, + max_members: self.max_members, + global_max_commission: self.global_max_commission, + } + .assimilate_storage(&mut storage); + + let mut ext = sp_io::TestExternalities::from(storage); + + ext.execute_with(|| { + // for events to be deposited. + frame_system::Pallet::::set_block_number(1); + + // 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)); + assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); + let last_pool = LastPoolId::::get(); + for (account_id, bonded) in self.members { + Balances::make_free_balance_be(&account_id, bonded * 2); + assert_ok!(Pools::join(RawOrigin::Signed(account_id).into(), bonded, last_pool)); + } + }); + + ext + } + + pub fn build_and_execute(self, test: impl FnOnce()) { + self.build().execute_with(|| { + test(); + Pools::do_try_state(CheckLevel::get()).unwrap(); + }) + } +} + +pub fn unsafe_set_state(pool_id: PoolId, state: PoolState) { + BondedPools::::try_mutate(pool_id, |maybe_bonded_pool| { + maybe_bonded_pool.as_mut().ok_or(()).map(|bonded_pool| { + bonded_pool.state = state; + }) + }) + .unwrap() +} + +parameter_types! { + storage PoolsEvents: u32 = 0; + storage BalancesEvents: u32 = 0; +} + +/// Helper to run a specified amount of blocks. +pub fn run_blocks(n: u64) { + let current_block = System::block_number(); + run_to_block(n + current_block); +} + +/// Helper to run to a specific block. +pub fn run_to_block(n: u64) { + let current_block = System::block_number(); + assert!(n > current_block); + while System::block_number() < n { + Pools::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Pools::on_initialize(System::block_number()); + } +} + +/// All events of this pallet. +pub fn pool_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Pools(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = PoolsEvents::get(); + PoolsEvents::set(&(events.len() as u32)); + events.into_iter().skip(already_seen as usize).collect() +} + +/// All events of the `Balances` pallet. +pub fn balances_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Balances(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = BalancesEvents::get(); + BalancesEvents::set(&(events.len() as u32)); + events.into_iter().skip(already_seen as usize).collect() +} + +/// Same as `fully_unbond`, in permissioned setting. +pub fn fully_unbond_permissioned(member: AccountId) -> DispatchResult { + let points = PoolMembers::::get(member) + .map(|d| d.active_points()) + .unwrap_or_default(); + Pools::unbond(RuntimeOrigin::signed(member), member, points) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn u256_to_balance_convert_works() { + assert_eq!(U256ToBalance::convert(0u32.into()), Zero::zero()); + assert_eq!(U256ToBalance::convert(Balance::max_value().into()), Balance::max_value()) + } + + #[test] + #[should_panic] + fn u256_to_balance_convert_panics_correctly() { + U256ToBalance::convert(U256::from(Balance::max_value()).saturating_add(1u32.into())); + } + + #[test] + fn balance_to_u256_convert_works() { + assert_eq!(BalanceToU256::convert(0u32.into()), U256::zero()); + assert_eq!(BalanceToU256::convert(Balance::max_value()), Balance::max_value().into()) + } +} diff --git a/pallets/nomination-pools/src/tests.rs b/pallets/nomination-pools/src/tests.rs new file mode 100644 index 000000000..ac8fa5c4d --- /dev/null +++ b/pallets/nomination-pools/src/tests.rs @@ -0,0 +1,6729 @@ +// This file is part of Substrate. + +// Copyright (C) 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 super::*; +use crate::{mock::*, Event}; +use frame_support::{assert_err, assert_noop, assert_ok, assert_storage_noop, bounded_btree_map}; +use pallet_balances::Event as BEvent; +use sp_runtime::{traits::Dispatchable, FixedU128}; + +macro_rules! unbonding_pools_with_era { + ($($k:expr => $v:expr),* $(,)?) => {{ + use sp_std::iter::{Iterator, IntoIterator}; + let not_bounded: BTreeMap<_, _> = Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*])); + BoundedBTreeMap::, TotalUnbondingPools>::try_from(not_bounded).unwrap() + }}; +} + +macro_rules! member_unbonding_eras { + ($( $any:tt )*) => {{ + let x: BoundedBTreeMap = bounded_btree_map!($( $any )*); + x + }}; +} + +pub const DEFAULT_ROLES: PoolRoles = + PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), bouncer: Some(902) }; + +fn deposit_rewards(r: u128) { + let b = Balances::free_balance(&default_reward_account()).checked_add(r).unwrap(); + Balances::make_free_balance_be(&default_reward_account(), b); +} + +fn remove_rewards(r: u128) { + let b = Balances::free_balance(&default_reward_account()).checked_sub(r).unwrap(); + Balances::make_free_balance_be(&default_reward_account(), b); +} + +#[test] +fn test_setup_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(BondedPools::::count(), 1); + assert_eq!(RewardPools::::count(), 1); + assert_eq!(SubPoolsStorage::::count(), 0); + assert_eq!(PoolMembers::::count(), 1); + assert_eq!(StakingMock::bonding_duration(), 3); + assert!(Metadata::::contains_key(1)); + + let last_pool = LastPoolId::::get(); + assert_eq!( + BondedPool::::get(last_pool).unwrap(), + BondedPool:: { + id: last_pool, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 10, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + ); + assert_eq!( + RewardPools::::get(last_pool).unwrap(), + RewardPool:: { + last_recorded_reward_counter: Zero::zero(), + last_recorded_total_payouts: 0, + total_rewards_claimed: 0, + total_commission_claimed: 0, + total_commission_pending: 0, + } + ); + assert_eq!( + PoolMembers::::get(10).unwrap(), + PoolMember:: { pool_id: last_pool, points: 10, ..Default::default() } + ); + + let bonded_account = Pools::create_bonded_account(last_pool); + let reward_account = Pools::create_reward_account(last_pool); + + // the bonded_account should be bonded by the depositor's funds. + assert_eq!(StakingMock::active_stake(&bonded_account).unwrap(), 10); + assert_eq!(StakingMock::total_stake(&bonded_account).unwrap(), 10); + + // but not nominating yet. + assert!(Nominations::get().is_none()); + + // reward account should have an initial ED in it. + assert_eq!(Balances::free_balance(&reward_account), Balances::minimum_balance()); + }) +} + +mod bonded_pool { + use super::*; + #[test] + fn balance_to_point_works() { + ExtBuilder::default().build_and_execute(|| { + let mut bonded_pool = BondedPool:: { + id: 123123, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 100, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + + // 1 points : 1 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.balance_to_point(10), 10); + assert_eq!(bonded_pool.balance_to_point(0), 0); + + // 2 points : 1 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 50); + assert_eq!(bonded_pool.balance_to_point(10), 20); + + // 1 points : 2 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + bonded_pool.points = 50; + assert_eq!(bonded_pool.balance_to_point(10), 5); + + // 100 points : 0 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 0); + bonded_pool.points = 100; + assert_eq!(bonded_pool.balance_to_point(10), 100 * 10); + + // 0 points : 100 balance + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + bonded_pool.points = 0; + assert_eq!(bonded_pool.balance_to_point(10), 10); + + // 10 points : 3 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 30); + bonded_pool.points = 100; + assert_eq!(bonded_pool.balance_to_point(10), 33); + + // 2 points : 3 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 300); + bonded_pool.points = 200; + assert_eq!(bonded_pool.balance_to_point(10), 6); + + // 4 points : 9 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 900); + bonded_pool.points = 400; + assert_eq!(bonded_pool.balance_to_point(90), 40); + }) + } + + #[test] + fn points_to_balance_works() { + ExtBuilder::default().build_and_execute(|| { + // 1 balance : 1 points ratio + let mut bonded_pool = BondedPool:: { + id: 123123, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 100, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.points_to_balance(10), 10); + assert_eq!(bonded_pool.points_to_balance(0), 0); + + // 2 balance : 1 points ratio + bonded_pool.points = 50; + assert_eq!(bonded_pool.points_to_balance(10), 20); + + // 100 balance : 0 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + bonded_pool.points = 0; + assert_eq!(bonded_pool.points_to_balance(10), 0); + + // 0 balance : 100 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 0); + bonded_pool.points = 100; + assert_eq!(bonded_pool.points_to_balance(10), 0); + + // 10 balance : 3 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + bonded_pool.points = 30; + assert_eq!(bonded_pool.points_to_balance(10), 33); + + // 2 balance : 3 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 200); + bonded_pool.points = 300; + assert_eq!(bonded_pool.points_to_balance(10), 6); + + // 4 balance : 9 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 400); + bonded_pool.points = 900; + assert_eq!(bonded_pool.points_to_balance(90), 40); + }) + } + + #[test] + fn api_points_to_balance_works() { + ExtBuilder::default().build_and_execute(|| { + assert!(BondedPool::::get(1).is_some()); + assert_eq!(Pallet::::api_points_to_balance(1, 10), 10); + + // slash half of the pool's balance. expected result of `fn api_points_to_balance` + // to be 1/2 of the pool's balance. + StakingMock::set_bonded_balance( + default_bonded_account(), + Pools::depositor_min_bond() / 2, + ); + assert_eq!(Pallet::::api_points_to_balance(1, 10), 5); + + // if pool does not exist, points to balance ratio is 0. + assert_eq!(BondedPool::::get(2), None); + assert_eq!(Pallet::::api_points_to_balance(2, 10), 0); + }) + } + + #[test] + fn api_balance_to_points_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(Pallet::::api_balance_to_points(1, 0), 0); + assert_eq!(Pallet::::api_balance_to_points(1, 10), 10); + + // slash half of the pool's balance. expect result of `fn api_balance_to_points` + // to be 2 * of the balance to add to the pool. + StakingMock::set_bonded_balance( + default_bonded_account(), + Pools::depositor_min_bond() / 2, + ); + assert_eq!(Pallet::::api_balance_to_points(1, 10), 20); + + // if pool does not exist, balance to points ratio is 0. + assert_eq!(BondedPool::::get(2), None); + assert_eq!(Pallet::::api_points_to_balance(2, 10), 0); + }) + } + + #[test] + fn ok_to_join_with_works() { + ExtBuilder::default().build_and_execute(|| { + let pool = BondedPool:: { + id: 123, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 100, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + + let max_points_to_balance: u128 = + <::MaxPointsToBalance as Get>::get().into(); + + // Simulate a 100% slashed pool + StakingMock::set_bonded_balance(pool.bonded_account(), 0); + assert_noop!(pool.ok_to_join(), Error::::OverflowRisk); + + // Simulate a slashed pool at `MaxPointsToBalance` + 1 slashed pool + StakingMock::set_bonded_balance( + pool.bonded_account(), + max_points_to_balance.saturating_add(1), + ); + assert_ok!(pool.ok_to_join()); + + // Simulate a slashed pool at `MaxPointsToBalance` + StakingMock::set_bonded_balance(pool.bonded_account(), max_points_to_balance); + assert_noop!(pool.ok_to_join(), Error::::OverflowRisk); + + StakingMock::set_bonded_balance( + pool.bonded_account(), + Balance::MAX / max_points_to_balance, + ); + + // and a sanity check + StakingMock::set_bonded_balance( + pool.bonded_account(), + Balance::MAX / max_points_to_balance - 1, + ); + assert_ok!(pool.ok_to_join()); + }); + } +} + +mod reward_pool { + #[test] + fn current_balance_only_counts_balance_over_existential_deposit() { + use super::*; + + ExtBuilder::default().build_and_execute(|| { + let reward_account = Pools::create_reward_account(2); + + // Given + assert_eq!(Balances::free_balance(&reward_account), 0); + + // Then + assert_eq!(RewardPool::::current_balance(2), 0); + + // Given + Balances::make_free_balance_be(&reward_account, Balances::minimum_balance()); + + // Then + assert_eq!(RewardPool::::current_balance(2), 0); + + // Given + Balances::make_free_balance_be(&reward_account, Balances::minimum_balance() + 1); + + // Then + assert_eq!(RewardPool::::current_balance(2), 1); + }); + } +} + +mod unbond_pool { + use super::*; + + #[test] + fn points_to_issue_works() { + ExtBuilder::default().build_and_execute(|| { + // 1 points : 1 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 100 }; + assert_eq!(unbond_pool.balance_to_point(10), 10); + assert_eq!(unbond_pool.balance_to_point(0), 0); + + // 2 points : 1 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 50 }; + assert_eq!(unbond_pool.balance_to_point(10), 20); + + // 1 points : 2 balance ratio + let unbond_pool = UnbondPool:: { points: 50, balance: 100 }; + assert_eq!(unbond_pool.balance_to_point(10), 5); + + // 100 points : 0 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 0 }; + assert_eq!(unbond_pool.balance_to_point(10), 100 * 10); + + // 0 points : 100 balance + let unbond_pool = UnbondPool:: { points: 0, balance: 100 }; + assert_eq!(unbond_pool.balance_to_point(10), 10); + + // 10 points : 3 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 30 }; + assert_eq!(unbond_pool.balance_to_point(10), 33); + + // 2 points : 3 balance ratio + let unbond_pool = UnbondPool:: { points: 200, balance: 300 }; + assert_eq!(unbond_pool.balance_to_point(10), 6); + + // 4 points : 9 balance ratio + let unbond_pool = UnbondPool:: { points: 400, balance: 900 }; + assert_eq!(unbond_pool.balance_to_point(90), 40); + }) + } + + #[test] + fn balance_to_unbond_works() { + // 1 balance : 1 points ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 10); + assert_eq!(unbond_pool.point_to_balance(0), 0); + + // 1 balance : 2 points ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 50 }; + assert_eq!(unbond_pool.point_to_balance(10), 5); + + // 2 balance : 1 points ratio + let unbond_pool = UnbondPool:: { points: 50, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 20); + + // 100 balance : 0 points ratio + let unbond_pool = UnbondPool:: { points: 0, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 0); + + // 0 balance : 100 points ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 0 }; + assert_eq!(unbond_pool.point_to_balance(10), 0); + + // 10 balance : 3 points ratio + let unbond_pool = UnbondPool:: { points: 30, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 33); + + // 2 balance : 3 points ratio + let unbond_pool = UnbondPool:: { points: 300, balance: 200 }; + assert_eq!(unbond_pool.point_to_balance(10), 6); + + // 4 balance : 9 points ratio + let unbond_pool = UnbondPool:: { points: 900, balance: 400 }; + assert_eq!(unbond_pool.point_to_balance(90), 40); + } +} + +mod sub_pools { + use super::*; + + #[test] + fn maybe_merge_pools_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(TotalUnbondingPools::::get(), 5); + assert_eq!(BondingDuration::get(), 3); + assert_eq!(PostUnbondingPoolsWindow::get(), 2); + + // Given + let mut sub_pool_0 = SubPools:: { + no_era: UnbondPool::::default(), + with_era: unbonding_pools_with_era! { + 0 => UnbondPool:: { points: 10, balance: 10 }, + 1 => UnbondPool:: { points: 10, balance: 10 }, + 2 => UnbondPool:: { points: 20, balance: 20 }, + 3 => UnbondPool:: { points: 30, balance: 30 }, + 4 => UnbondPool:: { points: 40, balance: 40 }, + }, + }; + + // When `current_era < TotalUnbondingPools`, + let sub_pool_1 = sub_pool_0.clone().maybe_merge_pools(0); + + // Then it exits early without modifications + assert_eq!(sub_pool_1, sub_pool_0); + + // When `current_era == TotalUnbondingPools`, + let sub_pool_1 = sub_pool_1.maybe_merge_pools(1); + + // Then it exits early without modifications + assert_eq!(sub_pool_1, sub_pool_0); + + // When `current_era - TotalUnbondingPools == 0`, + let mut sub_pool_1 = sub_pool_1.maybe_merge_pools(2); + + // Then era 0 is merged into the `no_era` pool + sub_pool_0.no_era = sub_pool_0.with_era.remove(&0).unwrap(); + assert_eq!(sub_pool_1, sub_pool_0); + + // Given we have entries for era 1..=5 + sub_pool_1 + .with_era + .try_insert(5, UnbondPool:: { points: 50, balance: 50 }) + .unwrap(); + sub_pool_0 + .with_era + .try_insert(5, UnbondPool:: { points: 50, balance: 50 }) + .unwrap(); + + // When `current_era - TotalUnbondingPools == 1` + let sub_pool_2 = sub_pool_1.maybe_merge_pools(3); + let era_1_pool = sub_pool_0.with_era.remove(&1).unwrap(); + + // Then era 1 is merged into the `no_era` pool + sub_pool_0.no_era.points += era_1_pool.points; + sub_pool_0.no_era.balance += era_1_pool.balance; + assert_eq!(sub_pool_2, sub_pool_0); + + // When `current_era - TotalUnbondingPools == 5`, so all pools with era <= 4 are removed + let sub_pool_3 = sub_pool_2.maybe_merge_pools(7); + + // Then all eras <= 5 are merged into the `no_era` pool + for era in 2..=5 { + let to_merge = sub_pool_0.with_era.remove(&era).unwrap(); + sub_pool_0.no_era.points += to_merge.points; + sub_pool_0.no_era.balance += to_merge.balance; + } + assert_eq!(sub_pool_3, sub_pool_0); + }); + } +} + +mod join { + use sp_runtime::TokenError; + + use super::*; + + #[test] + fn join_works() { + let bonded = |points, member_counter| BondedPool:: { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter, + points, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + ExtBuilder::default().with_check(0).build_and_execute(|| { + // Given + Balances::make_free_balance_be(&11, ExistentialDeposit::get() + 2); + assert!(!PoolMembers::::contains_key(11)); + + // When + assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1)); + + // Then + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, + ] + ); + + assert_eq!( + PoolMembers::::get(11).unwrap(), + PoolMember:: { pool_id: 1, points: 2, ..Default::default() } + ); + assert_eq!(BondedPool::::get(1).unwrap(), bonded(12, 2)); + + // Given + // The bonded balance is slashed in half + StakingMock::set_bonded_balance(Pools::create_bonded_account(1), 6); + + // And + Balances::make_free_balance_be(&12, ExistentialDeposit::get() + 12); + assert!(!PoolMembers::::contains_key(12)); + + // When + assert_ok!(Pools::join(RuntimeOrigin::signed(12), 12, 1)); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true }] + ); + + assert_eq!( + PoolMembers::::get(12).unwrap(), + PoolMember:: { pool_id: 1, points: 24, ..Default::default() } + ); + assert_eq!(BondedPool::::get(1).unwrap(), bonded(12 + 24, 3)); + }); + } + + #[test] + fn join_errors_correctly() { + ExtBuilder::default().with_check(0).build_and_execute(|| { + // 10 is already part of the default pool created. + assert_eq!(PoolMembers::::get(10).unwrap().pool_id, 1); + + assert_noop!( + Pools::join(RuntimeOrigin::signed(10), 420, 123), + Error::::AccountBelongsToOtherPool + ); + + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), 420, 123), + Error::::PoolNotFound + ); + + // Force the pools bonded balance to 0, simulating a 100% slash + StakingMock::set_bonded_balance(Pools::create_bonded_account(1), 0); + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), 420, 1), + Error::::OverflowRisk + ); + + // Given a mocked bonded pool + BondedPool:: { + id: 123, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 100, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + + // and reward pool + RewardPools::::insert(123, RewardPool:: { ..Default::default() }); + + // Force the points:balance ratio to `MaxPointsToBalance` (100/10) + let max_points_to_balance: u128 = + <::MaxPointsToBalance as Get>::get().into(); + + StakingMock::set_bonded_balance( + Pools::create_bonded_account(123), + max_points_to_balance, + ); + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), 420, 123), + Error::::OverflowRisk + ); + + StakingMock::set_bonded_balance( + Pools::create_bonded_account(123), + Balance::MAX / max_points_to_balance, + ); + // Balance needs to be gt Balance::MAX / `MaxPointsToBalance` + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), 5, 123), + TokenError::FundsUnavailable, + ); + + StakingMock::set_bonded_balance(Pools::create_bonded_account(1), max_points_to_balance); + + // Cannot join a pool that isn't open + unsafe_set_state(123, PoolState::Blocked); + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), max_points_to_balance, 123), + Error::::NotOpen + ); + + unsafe_set_state(123, PoolState::Destroying); + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), max_points_to_balance, 123), + Error::::NotOpen + ); + + // Given + MinJoinBond::::put(100); + + // Then + assert_noop!( + Pools::join(RuntimeOrigin::signed(11), 99, 123), + Error::::MinimumBondNotMet + ); + }); + } + + #[test] + #[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] + #[cfg_attr(not(debug_assertions), should_panic)] + fn join_panics_when_reward_pool_not_found() { + ExtBuilder::default().build_and_execute(|| { + StakingMock::set_bonded_balance(Pools::create_bonded_account(123), 100); + BondedPool:: { + id: 123, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 100, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + let _ = Pools::join(RuntimeOrigin::signed(11), 420, 123); + }); + } + + #[test] + fn join_max_member_limits_are_respected() { + ExtBuilder::default().build_and_execute(|| { + // Given + assert_eq!(MaxPoolMembersPerPool::::get(), Some(3)); + for i in 1..3 { + let account = i + 100; + Balances::make_free_balance_be(&account, 100 + Balances::minimum_balance()); + + assert_ok!(Pools::join(RuntimeOrigin::signed(account), 100, 1)); + } + + Balances::make_free_balance_be(&103, 100 + Balances::minimum_balance()); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 101, pool_id: 1, bonded: 100, joined: true }, + Event::Bonded { member: 102, pool_id: 1, bonded: 100, joined: true } + ] + ); + + assert_noop!( + Pools::join(RuntimeOrigin::signed(103), 100, 1), + Error::::MaxPoolMembers + ); + + // Given + assert_eq!(PoolMembers::::count(), 3); + assert_eq!(MaxPoolMembers::::get(), Some(4)); + + Balances::make_free_balance_be(&104, 100 + Balances::minimum_balance()); + assert_ok!(Pools::create(RuntimeOrigin::signed(104), 100, 104, 104, 104)); + + let pool_account = BondedPools::::iter() + .find(|(_, bonded_pool)| bonded_pool.roles.depositor == 104) + .map(|(pool_account, _)| pool_account) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 104, pool_id: 2 }, + Event::Bonded { member: 104, pool_id: 2, bonded: 100, joined: true } + ] + ); + + assert_noop!( + Pools::join(RuntimeOrigin::signed(103), 100, pool_account), + Error::::MaxPoolMembers + ); + }); + } +} + +mod claim_payout { + use super::*; + + fn del(points: Balance, last_recorded_reward_counter: u128) -> PoolMember { + PoolMember { + pool_id: 1, + points, + last_recorded_reward_counter: last_recorded_reward_counter.into(), + unbonding_eras: Default::default(), + } + } + + fn del_float(points: Balance, last_recorded_reward_counter: f64) -> PoolMember { + PoolMember { + pool_id: 1, + points, + last_recorded_reward_counter: RewardCounter::from_float(last_recorded_reward_counter), + unbonding_eras: Default::default(), + } + } + + fn rew( + last_recorded_reward_counter: u128, + last_recorded_total_payouts: Balance, + total_rewards_claimed: Balance, + ) -> RewardPool { + RewardPool { + last_recorded_reward_counter: last_recorded_reward_counter.into(), + last_recorded_total_payouts, + total_rewards_claimed, + total_commission_claimed: 0, + total_commission_pending: 0, + } + } + + #[test] + fn claim_payout_works() { + ExtBuilder::default() + .add_members(vec![(40, 40), (50, 50)]) + .build_and_execute(|| { + // Given each member currently has a free balance of + Balances::make_free_balance_be(&10, 0); + Balances::make_free_balance_be(&40, 0); + Balances::make_free_balance_be(&50, 0); + let ed = Balances::minimum_balance(); + + // and the reward pool has earned 100 in rewards + assert_eq!(Balances::free_balance(default_reward_account()), ed); + deposit_rewards(100); + + let _ = pool_events_since_last_call(); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 },] + ); + // last recorded reward counter at the time of this member's payout is 1 + assert_eq!(PoolMembers::::get(10).unwrap(), del(10, 1)); + // pool's 'last_recorded_reward_counter' and 'last_recorded_total_payouts' don't + // really change unless if someone bonds/unbonds. + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 10)); + assert_eq!(Balances::free_balance(&10), 10); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 90); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(40))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 40, pool_id: 1, payout: 40 }] + ); + assert_eq!(PoolMembers::::get(40).unwrap(), del(40, 1)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 50)); + assert_eq!(Balances::free_balance(&40), 40); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 50); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(50))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] + ); + assert_eq!(PoolMembers::::get(50).unwrap(), del(50, 1)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 100)); + assert_eq!(Balances::free_balance(&50), 50); + assert_eq!(Balances::free_balance(&default_reward_account()), ed); + + // Given the reward pool has some new rewards + deposit_rewards(50); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] + ); + assert_eq!(PoolMembers::::get(10).unwrap(), del_float(10, 1.5)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 105)); + assert_eq!(Balances::free_balance(&10), 10 + 5); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 45); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(40))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 40, pool_id: 1, payout: 20 }] + ); + assert_eq!(PoolMembers::::get(40).unwrap(), del_float(40, 1.5)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 125)); + assert_eq!(Balances::free_balance(&40), 40 + 20); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 25); + + // Given del 50 hasn't claimed and the reward pools has just earned 50 + deposit_rewards(50); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 75); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(50))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] + ); + assert_eq!(PoolMembers::::get(50).unwrap(), del_float(50, 2.0)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 175)); + assert_eq!(Balances::free_balance(&50), 50 + 50); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 25); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] + ); + assert_eq!(PoolMembers::::get(10).unwrap(), del(10, 2)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 180)); + assert_eq!(Balances::free_balance(&10), 15 + 5); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 20); + + // Given del 40 hasn't claimed and the reward pool has just earned 400 + deposit_rewards(400); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 420); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 40 }] + ); + + // We expect a payout of 40 + assert_eq!(PoolMembers::::get(10).unwrap(), del(10, 6)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 220)); + assert_eq!(Balances::free_balance(&10), 20 + 40); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 380); + + // Given del 40 + del 50 haven't claimed and the reward pool has earned 20 + deposit_rewards(20); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 400); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 2 }] + ); + assert_eq!(PoolMembers::::get(10).unwrap(), del_float(10, 6.2)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 222)); + assert_eq!(Balances::free_balance(&10), 60 + 2); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 398); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(40))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 40, pool_id: 1, payout: 188 }] + ); + assert_eq!(PoolMembers::::get(40).unwrap(), del_float(40, 6.2)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 410)); + assert_eq!(Balances::free_balance(&40), 60 + 188); + assert_eq!(Balances::free_balance(&default_reward_account()), ed + 210); + + // When + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(50))); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 50, pool_id: 1, payout: 210 }] + ); + assert_eq!(PoolMembers::::get(50).unwrap(), del_float(50, 6.2)); + assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 620)); + assert_eq!(Balances::free_balance(&50), 100 + 210); + assert_eq!(Balances::free_balance(&default_reward_account()), ed); + }); + } + + #[test] + fn reward_payout_errors_if_a_member_is_fully_unbonding() { + ExtBuilder::default().add_members(vec![(11, 11)]).build_and_execute(|| { + // fully unbond the member. + assert_ok!(fully_unbond_permissioned(11)); + + assert_noop!( + Pools::claim_payout(RuntimeOrigin::signed(11)), + Error::::FullyUnbonding + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 11, joined: true }, + Event::Unbonded { member: 11, pool_id: 1, points: 11, balance: 11, era: 3 } + ] + ); + }); + } + + #[test] + fn claim_payout_bounds_commission_above_global() { + ExtBuilder::default().build_and_execute(|| { + let (mut member, bonded_pool, mut reward_pool) = + Pools::get_member_with_pools(&10).unwrap(); + + // top up commission payee account to existential deposit + let _ = Balances::deposit_creating(&2, 5); + + // Set a commission pool 1 to 75%, with a payee set to `2` + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + bonded_pool.id, + Some((Perbill::from_percent(75), 2)), + )); + + // re-introduce the global maximum to 50% - 25% lower than the current commission of the + // pool. + GlobalMaxCommission::::set(Some(Perbill::from_percent(50))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(75), 2)) + } + ] + ); + + // The pool earns 10 points + deposit_rewards(10); + + assert_ok!(Pools::do_reward_payout( + &10, + &mut member, + &mut BondedPool::::get(1).unwrap(), + &mut reward_pool + )); + + // commission applied is 50%, not 75%. Has been bounded by `GlobalMaxCommission`. + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 },] + ); + }) + } + + #[test] + fn do_reward_payout_works_with_a_pool_of_1() { + let del = |last_recorded_reward_counter| del_float(10, last_recorded_reward_counter); + + ExtBuilder::default().build_and_execute(|| { + let (mut member, mut bonded_pool, mut reward_pool) = + Pools::get_member_with_pools(&10).unwrap(); + let ed = Balances::minimum_balance(); + + let payout = + Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!(payout, 0); + assert_eq!(member, del(0.0)); + assert_eq!(reward_pool, rew(0, 0, 0)); + + // Given the pool has earned some rewards for the first time + deposit_rewards(5); + + // When + let payout = + Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 5 } + ] + ); + assert_eq!(payout, 5); + assert_eq!(reward_pool, rew(0, 0, 5)); + assert_eq!(member, del(0.5)); + + // Given the pool has earned rewards again + deposit_rewards(10); + + // When + let payout = + Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 }] + ); + assert_eq!(payout, 10); + assert_eq!(reward_pool, rew(0, 0, 15)); + assert_eq!(member, del(1.5)); + + // Given the pool has earned no new rewards + Balances::make_free_balance_be(&default_reward_account(), ed); + + // When + let payout = + Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!(pool_events_since_last_call(), vec![]); + assert_eq!(payout, 0); + assert_eq!(reward_pool, rew(0, 0, 15)); + assert_eq!(member, del(1.5)); + }); + } + + #[test] + fn do_reward_payout_works_with_a_pool_of_3() { + ExtBuilder::default() + .add_members(vec![(40, 40), (50, 50)]) + .build_and_execute(|| { + let mut bonded_pool = BondedPool::::get(1).unwrap(); + let mut reward_pool = RewardPools::::get(1).unwrap(); + + let mut del_10 = PoolMembers::::get(10).unwrap(); + let mut del_40 = PoolMembers::::get(40).unwrap(); + let mut del_50 = PoolMembers::::get(50).unwrap(); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 50, pool_id: 1, bonded: 50, joined: true } + ] + ); + + // Given we have a total of 100 points split among the members + assert_eq!(del_50.points + del_40.points + del_10.points, 100); + assert_eq!(bonded_pool.points, 100); + + // and the reward pool has earned 100 in rewards + deposit_rewards(100); + + // When + let payout = + Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 }] + ); + assert_eq!(payout, 10); + assert_eq!(del_10, del(10, 1)); + assert_eq!(reward_pool, rew(0, 0, 10)); + + // When + let payout = + Pools::do_reward_payout(&40, &mut del_40, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 40, pool_id: 1, payout: 40 }] + ); + assert_eq!(payout, 40); + assert_eq!(del_40, del(40, 1)); + assert_eq!(reward_pool, rew(0, 0, 50)); + + // When + let payout = + Pools::do_reward_payout(&50, &mut del_50, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] + ); + assert_eq!(payout, 50); + assert_eq!(del_50, del(50, 1)); + assert_eq!(reward_pool, rew(0, 0, 100)); + + // Given the reward pool has some new rewards + deposit_rewards(50); + + // When + let payout = + Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] + ); + assert_eq!(payout, 5); + assert_eq!(del_10, del_float(10, 1.5)); + assert_eq!(reward_pool, rew(0, 0, 105)); + + // When + let payout = + Pools::do_reward_payout(&40, &mut del_40, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 40, pool_id: 1, payout: 20 }] + ); + assert_eq!(payout, 20); + assert_eq!(del_40, del_float(40, 1.5)); + assert_eq!(reward_pool, rew(0, 0, 125)); + + // Given del_50 hasn't claimed and the reward pools has just earned 50 + deposit_rewards(50); + + // When + let payout = + Pools::do_reward_payout(&50, &mut del_50, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] + ); + assert_eq!(payout, 50); + assert_eq!(del_50, del_float(50, 2.0)); + assert_eq!(reward_pool, rew(0, 0, 175)); + + // When + let payout = + Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] + ); + assert_eq!(payout, 5); + assert_eq!(del_10, del_float(10, 2.0)); + assert_eq!(reward_pool, rew(0, 0, 180)); + + // Given del_40 hasn't claimed and the reward pool has just earned 400 + deposit_rewards(400); + + // When + let payout = + Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 40 }] + ); + assert_eq!(payout, 40); + assert_eq!(del_10, del_float(10, 6.0)); + assert_eq!(reward_pool, rew(0, 0, 220)); + + // Given del_40 + del_50 haven't claimed and the reward pool has earned 20 + deposit_rewards(20); + + // When + let payout = + Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!(payout, 2); + assert_eq!(del_10, del_float(10, 6.2)); + assert_eq!(reward_pool, rew(0, 0, 222)); + + // When + let payout = + Pools::do_reward_payout(&40, &mut del_40, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!(payout, 188); // 20 (from the 50) + 160 (from the 400) + 8 (from the 20) + assert_eq!(del_40, del_float(40, 6.2)); + assert_eq!(reward_pool, rew(0, 0, 410)); + + // When + let payout = + Pools::do_reward_payout(&50, &mut del_50, &mut bonded_pool, &mut reward_pool) + .unwrap(); + + // Then + assert_eq!(payout, 210); // 200 (from the 400) + 10 (from the 20) + assert_eq!(del_50, del_float(50, 6.2)); + assert_eq!(reward_pool, rew(0, 0, 620)); + }); + } + + #[test] + fn rewards_distribution_is_fair_basic() { + ExtBuilder::default().build_and_execute(|| { + // reward pool by 10. + deposit_rewards(10); + + // 20 joins afterwards. + Balances::make_free_balance_be(&20, Balances::minimum_balance() + 10); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); + + // reward by another 20 + deposit_rewards(20); + + // 10 should claim 10 + 10, 20 should claim 20 / 2. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, + ] + ); + + // any upcoming rewards are shared equally. + deposit_rewards(20); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, + ] + ); + }); + } + + #[test] + fn rewards_distribution_is_fair_basic_with_fractions() { + // basically checks the case where the amount of rewards is less than the pool shares. for + // this, we have to rely on fixed point arithmetic. + ExtBuilder::default().build_and_execute(|| { + deposit_rewards(3); + + Balances::make_free_balance_be(&20, Balances::minimum_balance() + 10); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); + + deposit_rewards(6); + + // 10 should claim 3, 20 should claim 3 + 3. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 3 + 3 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 3 }, + ] + ); + + // any upcoming rewards are shared equally. + deposit_rewards(8); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 4 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 4 }, + ] + ); + + // uneven upcoming rewards are shared equally, rounded down. + deposit_rewards(7); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 3 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 3 }, + ] + ); + }); + } + + #[test] + fn rewards_distribution_is_fair_3() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + deposit_rewards(30); + + Balances::make_free_balance_be(&20, ed + 10); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); + + deposit_rewards(100); + + Balances::make_free_balance_be(&30, ed + 10); + assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); + + deposit_rewards(60); + + // 10 should claim 10, 20 should claim nothing. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 30 + 100 / 2 + 60 / 3 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 100 / 2 + 60 / 3 }, + Event::PaidOut { member: 30, pool_id: 1, payout: 60 / 3 }, + ] + ); + + // any upcoming rewards are shared equally. + deposit_rewards(30); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, + Event::PaidOut { member: 30, pool_id: 1, payout: 10 }, + ] + ); + }); + } + + #[test] + fn pending_rewards_per_member_works() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + assert_eq!(Pools::api_pending_rewards(10), Some(0)); + deposit_rewards(30); + assert_eq!(Pools::api_pending_rewards(10), Some(30)); + assert_eq!(Pools::api_pending_rewards(20), None); + + Balances::make_free_balance_be(&20, ed + 10); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); + + assert_eq!(Pools::api_pending_rewards(10), Some(30)); + assert_eq!(Pools::api_pending_rewards(20), Some(0)); + + deposit_rewards(100); + + assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50)); + assert_eq!(Pools::api_pending_rewards(20), Some(50)); + assert_eq!(Pools::api_pending_rewards(30), None); + + Balances::make_free_balance_be(&30, ed + 10); + assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); + + assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50)); + assert_eq!(Pools::api_pending_rewards(20), Some(50)); + assert_eq!(Pools::api_pending_rewards(30), Some(0)); + + deposit_rewards(60); + + assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50 + 20)); + assert_eq!(Pools::api_pending_rewards(20), Some(50 + 20)); + assert_eq!(Pools::api_pending_rewards(30), Some(20)); + + // 10 should claim 10, 20 should claim nothing. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_eq!(Pools::api_pending_rewards(10), Some(0)); + assert_eq!(Pools::api_pending_rewards(20), Some(50 + 20)); + assert_eq!(Pools::api_pending_rewards(30), Some(20)); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_eq!(Pools::api_pending_rewards(10), Some(0)); + assert_eq!(Pools::api_pending_rewards(20), Some(0)); + assert_eq!(Pools::api_pending_rewards(30), Some(20)); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + assert_eq!(Pools::api_pending_rewards(10), Some(0)); + assert_eq!(Pools::api_pending_rewards(20), Some(0)); + assert_eq!(Pools::api_pending_rewards(30), Some(0)); + }); + } + + #[test] + fn rewards_distribution_is_fair_bond_extra() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + Balances::make_free_balance_be(&20, ed + 20); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); + Balances::make_free_balance_be(&30, ed + 20); + assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); + + deposit_rewards(40); + + // everyone claims. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 20 }, + Event::PaidOut { member: 30, pool_id: 1, payout: 10 } + ] + ); + + // 30 now bumps itself to be like 20. + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(30), BondExtra::FreeBalance(10))); + + // more rewards come in. + deposit_rewards(100); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: false }, + Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 40 }, + Event::PaidOut { member: 30, pool_id: 1, payout: 40 } + ] + ); + }); + } + + #[test] + fn rewards_distribution_is_fair_unbond() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + Balances::make_free_balance_be(&20, ed + 20); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); + + deposit_rewards(30); + + // everyone claims. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 20 } + ] + ); + + // 20 unbonds to be equal to 10 (10 points each). + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + + // more rewards come in. + deposit_rewards(100); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 3 }, + Event::PaidOut { member: 10, pool_id: 1, payout: 50 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 50 }, + ] + ); + }); + } + + #[test] + fn unclaimed_reward_is_safe() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + Balances::make_free_balance_be(&20, ed + 20); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); + Balances::make_free_balance_be(&30, ed + 20); + assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); + + // 10 gets 10, 20 gets 20, 30 gets 10 + deposit_rewards(40); + + // some claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 20 } + ] + ); + + // 10 gets 20, 20 gets 40, 30 gets 20 + deposit_rewards(80); + + // some claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 40 } + ] + ); + + // 10 gets 20, 20 gets 40, 30 gets 20 + deposit_rewards(80); + + // some claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 40 } + ] + ); + + // now 30 claims all at once + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 30, pool_id: 1, payout: 10 + 20 + 20 }] + ); + }); + } + + #[test] + fn bond_extra_and_delayed_claim() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + Balances::make_free_balance_be(&20, ed + 200); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); + + // 10 gets 10, 20 gets 20, 30 gets 10 + deposit_rewards(30); + + // some claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 10 } + ] + ); + + // 20 has not claimed yet, more reward comes + deposit_rewards(60); + + // and 20 bonds more -- they should not have more share of this reward. + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(20), BondExtra::FreeBalance(10))); + + // everyone claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + // 20 + 40, which means the extra amount they bonded did not impact us. + Event::PaidOut { member: 20, pool_id: 1, payout: 60 }, + Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: false }, + Event::PaidOut { member: 10, pool_id: 1, payout: 20 } + ] + ); + + // but in the next round of rewards, the extra10 they bonded has an impact. + deposit_rewards(60); + + // everyone claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 15 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 45 } + ] + ); + }); + } + + #[test] + fn create_sets_recorded_data() { + ExtBuilder::default().build_and_execute(|| { + MaxPools::::set(None); + // pool 10 has already been created. + let (member_10, _, reward_pool_10) = Pools::get_member_with_pools(&10).unwrap(); + + assert_eq!(reward_pool_10.last_recorded_total_payouts, 0); + assert_eq!(reward_pool_10.total_rewards_claimed, 0); + assert_eq!(reward_pool_10.last_recorded_reward_counter, 0.into()); + + assert_eq!(member_10.last_recorded_reward_counter, 0.into()); + + // transfer some reward to pool 1. + deposit_rewards(60); + + // create pool 2 + Balances::make_free_balance_be(&20, 100); + assert_ok!(Pools::create(RuntimeOrigin::signed(20), 10, 20, 20, 20)); + + // has no impact -- initial + let (member_20, _, reward_pool_20) = Pools::get_member_with_pools(&20).unwrap(); + + assert_eq!(reward_pool_20.last_recorded_total_payouts, 0); + assert_eq!(reward_pool_20.total_rewards_claimed, 0); + assert_eq!(reward_pool_20.last_recorded_reward_counter, 0.into()); + + assert_eq!(member_20.last_recorded_reward_counter, 0.into()); + + // pre-fund the reward account of pool id 3 with some funds. + Balances::make_free_balance_be(&Pools::create_reward_account(3), 10); + + // create pool 3 + Balances::make_free_balance_be(&30, 100); + assert_ok!(Pools::create(RuntimeOrigin::signed(30), 10, 30, 30, 30)); + + // reward counter is still the same. + let (member_30, _, reward_pool_30) = Pools::get_member_with_pools(&30).unwrap(); + assert_eq!( + Balances::free_balance(&Pools::create_reward_account(3)), + 10 + Balances::minimum_balance() + ); + + assert_eq!(reward_pool_30.last_recorded_total_payouts, 0); + assert_eq!(reward_pool_30.total_rewards_claimed, 0); + assert_eq!(reward_pool_30.last_recorded_reward_counter, 0.into()); + + assert_eq!(member_30.last_recorded_reward_counter, 0.into()); + + // and 30 can claim the reward now. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Created { depositor: 20, pool_id: 2 }, + Event::Bonded { member: 20, pool_id: 2, bonded: 10, joined: true }, + Event::Created { depositor: 30, pool_id: 3 }, + Event::Bonded { member: 30, pool_id: 3, bonded: 10, joined: true }, + Event::PaidOut { member: 30, pool_id: 3, payout: 10 } + ] + ); + }) + } + + #[test] + fn join_updates_recorded_data() { + ExtBuilder::default().build_and_execute(|| { + MaxPoolMembers::::set(None); + MaxPoolMembersPerPool::::set(None); + let join = |x, y| { + Balances::make_free_balance_be(&x, y + Balances::minimum_balance()); + assert_ok!(Pools::join(RuntimeOrigin::signed(x), y, 1)); + }; + + { + let (member_10, _, reward_pool_10) = Pools::get_member_with_pools(&10).unwrap(); + + assert_eq!(reward_pool_10.last_recorded_total_payouts, 0); + assert_eq!(reward_pool_10.total_rewards_claimed, 0); + assert_eq!(reward_pool_10.last_recorded_reward_counter, 0.into()); + + assert_eq!(member_10.last_recorded_reward_counter, 0.into()); + } + + // someone joins without any rewards being issued. + { + join(20, 10); + let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); + // reward counter is 0 both before.. + assert_eq!(member.last_recorded_reward_counter, 0.into()); + assert_eq!(reward_pool.last_recorded_total_payouts, 0); + assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); + } + + // transfer some reward to pool 1. + deposit_rewards(60); + + { + join(30, 10); + let (member, _, reward_pool) = Pools::get_member_with_pools(&30).unwrap(); + assert_eq!(reward_pool.last_recorded_total_payouts, 60); + // explanation: we have a total of 20 points so far (excluding the 10 that just got + // bonded), and 60 unclaimed rewards. each share is then roughly worth of 3 units of + // rewards, thus reward counter is 3. member's reward counter is the same + assert_eq!(member.last_recorded_reward_counter, 3.into()); + assert_eq!(reward_pool.last_recorded_reward_counter, 3.into()); + } + + // someone else joins + { + join(40, 10); + let (member, _, reward_pool) = Pools::get_member_with_pools(&40).unwrap(); + // reward counter does not change since no rewards have came in. + assert_eq!(member.last_recorded_reward_counter, 3.into()); + assert_eq!(reward_pool.last_recorded_reward_counter, 3.into()); + assert_eq!(reward_pool.last_recorded_total_payouts, 60); + } + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 10, joined: true } + ] + ); + }) + } + + #[test] + fn bond_extra_updates_recorded_data() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + MaxPoolMembers::::set(None); + MaxPoolMembersPerPool::::set(None); + + // initial state of pool 1. + { + let (member_10, _, reward_pool_10) = Pools::get_member_with_pools(&10).unwrap(); + + assert_eq!(reward_pool_10.last_recorded_total_payouts, 0); + assert_eq!(reward_pool_10.total_rewards_claimed, 0); + assert_eq!(reward_pool_10.last_recorded_reward_counter, 0.into()); + + assert_eq!(member_10.last_recorded_reward_counter, 0.into()); + } + + Balances::make_free_balance_be(&10, 100); + Balances::make_free_balance_be(&20, 100); + + // 10 bonds extra without any rewards. + { + assert_ok!(Pools::bond_extra( + RuntimeOrigin::signed(10), + BondExtra::FreeBalance(10) + )); + let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); + assert_eq!(member.last_recorded_reward_counter, 0.into()); + assert_eq!(reward_pool.last_recorded_total_payouts, 0); + assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); + } + + // 10 bonds extra again with some rewards. This reward should be split equally between + // 10 and 20, as they both have equal points now. + deposit_rewards(30); + + { + assert_ok!(Pools::bond_extra( + RuntimeOrigin::signed(10), + BondExtra::FreeBalance(10) + )); + let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); + // explanation: before bond_extra takes place, there is 40 points and 30 balance in + // the system, RewardCounter is therefore 7.5 + assert_eq!(member.last_recorded_reward_counter, RewardCounter::from_float(0.75)); + assert_eq!( + reward_pool.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + assert_eq!(reward_pool.last_recorded_total_payouts, 30); + } + + // 20 bonds extra again, without further rewards. + { + assert_ok!(Pools::bond_extra( + RuntimeOrigin::signed(20), + BondExtra::FreeBalance(10) + )); + let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); + assert_eq!(member.last_recorded_reward_counter, RewardCounter::from_float(0.75)); + assert_eq!( + reward_pool.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + assert_eq!(reward_pool.last_recorded_total_payouts, 30); + } + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, + Event::PaidOut { member: 10, pool_id: 1, payout: 15 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, + Event::PaidOut { member: 20, pool_id: 1, payout: 15 }, + Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: false } + ] + ); + }) + } + + #[test] + fn bond_extra_pending_rewards_works() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + MaxPoolMembers::::set(None); + MaxPoolMembersPerPool::::set(None); + + // pool receives some rewards. + deposit_rewards(30); + System::reset_events(); + + // 10 cashes it out, and bonds it. + { + assert_ok!(Pools::claim_payout(RuntimeOrigin::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(RuntimeOrigin::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() + .add_members(vec![(20, 20), (30, 20)]) + .build_and_execute(|| { + MaxPoolMembers::::set(None); + MaxPoolMembersPerPool::::set(None); + + // initial state of pool 1. + { + let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); + + assert_eq!(reward_pool.last_recorded_total_payouts, 0); + assert_eq!(reward_pool.total_rewards_claimed, 0); + assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); + + assert_eq!(member.last_recorded_reward_counter, 0.into()); + } + + // 20 unbonds without any rewards. + { + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); + assert_eq!(member.last_recorded_reward_counter, 0.into()); + assert_eq!(reward_pool.last_recorded_total_payouts, 0); + assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); + } + + // some rewards come in. + deposit_rewards(30); + + // and 30 also unbonds half. + { + assert_ok!(Pools::unbond(RuntimeOrigin::signed(30), 30, 10)); + let (member, _, reward_pool) = Pools::get_member_with_pools(&30).unwrap(); + // 30 reward in the system, and 40 points before this unbond to collect it, + // RewardCounter is 3/4. + assert_eq!( + member.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + assert_eq!(reward_pool.last_recorded_total_payouts, 30); + assert_eq!( + reward_pool.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + } + + // 30 unbonds again, not change this time. + { + assert_ok!(Pools::unbond(RuntimeOrigin::signed(30), 30, 5)); + let (member, _, reward_pool) = Pools::get_member_with_pools(&30).unwrap(); + assert_eq!( + member.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + assert_eq!(reward_pool.last_recorded_total_payouts, 30); + assert_eq!( + reward_pool.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + } + + // 20 unbonds again, not change this time, just collecting their reward. + { + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); + let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); + assert_eq!( + member.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + assert_eq!(reward_pool.last_recorded_total_payouts, 30); + assert_eq!( + reward_pool.last_recorded_reward_counter, + RewardCounter::from_float(0.75) + ); + } + + // trigger 10's reward as well to see all of the payouts. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Bonded { member: 30, pool_id: 1, bonded: 20, joined: true }, + Event::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 3 }, + Event::PaidOut { member: 30, pool_id: 1, payout: 15 }, + Event::Unbonded { member: 30, pool_id: 1, balance: 10, points: 10, era: 3 }, + Event::Unbonded { member: 30, pool_id: 1, balance: 5, points: 5, era: 3 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 7 }, + Event::Unbonded { member: 20, pool_id: 1, balance: 5, points: 5, era: 3 }, + Event::PaidOut { member: 10, pool_id: 1, payout: 7 } + ] + ); + }) + } + + #[test] + fn rewards_are_rounded_down_depositor_collects_them() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + // initial balance of 10. + + assert_eq!(Balances::free_balance(&10), 35); + assert_eq!( + Balances::free_balance(&default_reward_account()), + Balances::minimum_balance() + ); + + // some rewards come in. + deposit_rewards(40); + + // everyone claims + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + // some dust (1) remains in the reward account. + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 13 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 26 } + ] + ); + + // start dismantling the pool. + assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); + assert_ok!(fully_unbond_permissioned(20)); + + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + assert_ok!(fully_unbond_permissioned(10)); + + CurrentEra::set(6); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 3 }, + Event::Withdrawn { member: 20, pool_id: 1, balance: 20, points: 20 }, + Event::MemberRemoved { pool_id: 1, member: 20 }, + Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 6 }, + Event::Withdrawn { member: 10, pool_id: 1, balance: 10, points: 10 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 } + ] + ); + + assert!(!Metadata::::contains_key(1)); + // original ed + ed put into reward account + reward + bond + dust. + assert_eq!(Balances::free_balance(&10), 35 + 5 + 13 + 10 + 1); + }) + } + + #[test] + fn claim_payout_large_numbers() { + let unit = 10u128.pow(12); // akin to KSM + ExistentialDeposit::set(unit); + StakingMinBond::set(unit * 1000); + + ExtBuilder::default() + .max_members(Some(4)) + .max_members_per_pool(Some(4)) + .add_members(vec![(20, 1500 * unit), (21, 2500 * unit), (22, 5000 * unit)]) + .build_and_execute(|| { + // some rewards come in. + assert_eq!(Balances::free_balance(&default_reward_account()), unit); + deposit_rewards(unit / 1000); + + // everyone claims + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(21))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(22))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 1000000000000000, + joined: true + }, + Event::Bonded { + member: 20, + pool_id: 1, + bonded: 1500000000000000, + joined: true + }, + Event::Bonded { + member: 21, + pool_id: 1, + bonded: 2500000000000000, + joined: true + }, + Event::Bonded { + member: 22, + pool_id: 1, + bonded: 5000000000000000, + joined: true + }, + Event::PaidOut { member: 10, pool_id: 1, payout: 100000000 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 150000000 }, + Event::PaidOut { member: 21, pool_id: 1, payout: 250000000 }, + Event::PaidOut { member: 22, pool_id: 1, payout: 500000000 } + ] + ); + }) + } + + #[test] + fn claim_payout_other_works() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + Balances::make_free_balance_be(&default_reward_account(), 8); + // ... of which only 3 are claimable to make sure the reward account does not die. + let claimable_reward = 8 - ExistentialDeposit::get(); + // NOTE: easier to read if we use 3, so let's use the number instead of variable. + assert_eq!(claimable_reward, 3, "test is correct if rewards are divisible by 3"); + + // given + assert_eq!(Balances::free_balance(10), 35); + + // Permissioned by default + assert_noop!( + Pools::claim_payout_other(RuntimeOrigin::signed(80), 10), + Error::::DoesNotHavePermission + ); + + assert_ok!(Pools::set_claim_permission( + RuntimeOrigin::signed(10), + ClaimPermission::PermissionlessWithdraw + )); + assert_ok!(Pools::claim_payout_other(RuntimeOrigin::signed(80), 10)); + + // then + assert_eq!(Balances::free_balance(10), 36); + assert_eq!(Balances::free_balance(&default_reward_account()), 7); + }) + } +} + +mod unbond { + use super::*; + + #[test] + fn member_unbond_open() { + // depositor in pool, pool state open + // - member unbond above limit + // - member unbonds to 0 + // - member cannot unbond between within limit and 0 + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + // can unbond to above limit + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); + assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 15); + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 5); + + // cannot go to below 10: + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(20), 20, 10), + Error::::MinimumBondNotMet + ); + + // Make permissionless + assert_eq!(ClaimPermissions::::get(10), ClaimPermission::Permissioned); + assert_ok!(Pools::set_claim_permission( + RuntimeOrigin::signed(20), + ClaimPermission::PermissionlessAll + )); + + // but can go to 0 + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 15)); + assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); + assert_eq!( + ClaimPermissions::::get(20), + ClaimPermission::PermissionlessAll + ); + }) + } + + #[test] + fn member_kicked() { + // depositor in pool, pool state blocked + // - member cannot be kicked to above limit + // - member cannot be kicked between within limit and 0 + // - member kicked to 0 + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + unsafe_set_state(1, PoolState::Blocked); + let kicker = DEFAULT_ROLES.bouncer.unwrap(); + + // cannot be kicked to above the limit. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(kicker), 20, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // cannot go to below 10: + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(kicker), 20, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // but they themselves can do an unbond + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); + assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 18); + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 2); + + // can be kicked to 0. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(kicker), 20, 18)); + assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); + }) + } + + #[test] + fn member_unbond_destroying() { + // depositor in pool, pool state destroying + // - member cannot be permissionlessly unbonded to above limit + // - member cannot be permissionlessly unbonded between within limit and 0 + // - member permissionlessly unbonded to 0 + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + unsafe_set_state(1, PoolState::Destroying); + let random = 123; + + // cannot be kicked to above the limit. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(random), 20, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // cannot go to below 10: + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(random), 20, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // but they themselves can do an unbond + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); + assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 18); + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 2); + + // but can go to 0 + assert_ok!(Pools::unbond(RuntimeOrigin::signed(random), 20, 18)); + assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); + }) + } + + #[test] + fn depositor_unbond_open() { + // depositor in pool, pool state open + // - depositor unbonds to above limit + // - depositor cannot unbond to below limit or 0 + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + assert_eq!(PoolMembers::::get(10).unwrap().points, 20); + + // can unbond to above the limit. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 15); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 5); + + // cannot go to below 10: + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 10), + Error::::MinimumBondNotMet + ); + + // cannot go to 0 either. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 15), + Error::::MinimumBondNotMet + ); + }) + } + + #[test] + fn depositor_kick() { + // depositor in pool, pool state blocked + // - depositor can never be kicked. + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + assert_eq!(PoolMembers::::get(10).unwrap().points, 20); + + // set the stage + unsafe_set_state(1, PoolState::Blocked); + let kicker = DEFAULT_ROLES.bouncer.unwrap(); + + // cannot be kicked to above limit. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(kicker), 10, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or below the limit + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(kicker), 10, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or 0. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(kicker), 10, 20), + Error::::DoesNotHavePermission + ); + + // they themselves cannot do it either + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 20), + Error::::MinimumBondNotMet + ); + }) + } + + #[test] + fn depositor_unbond_destroying_permissionless() { + // depositor can never be permissionlessly unbonded. + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + assert_eq!(PoolMembers::::get(10).unwrap().points, 20); + + // set the stage + unsafe_set_state(1, PoolState::Destroying); + let random = 123; + + // cannot be kicked to above limit. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(random), 10, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or below the limit + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(random), 10, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or 0. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(random), 10, 20), + Error::::DoesNotHavePermission + ); + + // they themselves can do it in this case though. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 20)); + }) + } + + #[test] + fn depositor_unbond_destroying_not_last_member() { + // deposit in pool, pool state destroying + // - depositor can never leave if there is another member in the pool. + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Pools::bond_extra( + RuntimeOrigin::signed(10), + BondExtra::FreeBalance(10) + )); + assert_eq!(PoolMembers::::get(10).unwrap().points, 20); + + // set the stage + unsafe_set_state(1, PoolState::Destroying); + + // can go above the limit + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 15); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 5); + + // but not below the limit + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 10), + Error::::MinimumBondNotMet + ); + + // and certainly not zero + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 15), + Error::::MinimumBondNotMet + ); + }) + } + + #[test] + fn depositor_unbond_destroying_last_member() { + // deposit in pool, pool state destroying + // - depositor can unbond to above limit always. + // - depositor cannot unbond to below limit if last. + // - depositor can unbond to 0 if last and destroying. + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + assert_eq!(PoolMembers::::get(10).unwrap().points, 20); + + // set the stage + unsafe_set_state(1, PoolState::Destroying); + + // can unbond to above the limit. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 15); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 5); + + // still cannot go to below limit + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 10), + Error::::MinimumBondNotMet + ); + + // can go to 0 too. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 15)); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 0); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 20); + }) + } + + #[test] + fn unbond_of_1_works() { + ExtBuilder::default().build_and_execute(|| { + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(10)); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool:: { points: 10, balance: 10 }} + ); + + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 0, + roles: DEFAULT_ROLES, + state: PoolState::Destroying, + } + } + ); + + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); + }); + } + + #[test] + fn unbond_of_3_works() { + ExtBuilder::default() + .add_members(vec![(40, 40), (550, 550)]) + .build_and_execute(|| { + let ed = Balances::minimum_balance(); + // Given a slash from 600 -> 100 + StakingMock::set_bonded_balance(default_bonded_account(), 100); + // and unclaimed rewards of 600. + Balances::make_free_balance_be(&default_reward_account(), ed + 600); + + // When + assert_ok!(fully_unbond_permissioned(40)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }} + ); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 3, + points: 560, + roles: DEFAULT_ROLES, + state: PoolState::Open, + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, + Event::PaidOut { member: 40, pool_id: 1, payout: 40 }, + Event::Unbonded { member: 40, pool_id: 1, points: 6, balance: 6, era: 3 } + ] + ); + + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 94); + assert_eq!( + PoolMembers::::get(40).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 6) + ); + assert_eq!(Balances::free_balance(&40), 40 + 40); // We claim rewards when unbonding + + // When + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(550)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 98, balance: 98 }} + ); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 3, + points: 10, + roles: DEFAULT_ROLES, + state: PoolState::Destroying, + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 2); + assert_eq!( + PoolMembers::::get(550).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 92) + ); + assert_eq!(Balances::free_balance(&550), 550 + 550); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 550, pool_id: 1, payout: 550 }, + Event::Unbonded { + member: 550, + pool_id: 1, + points: 92, + balance: 92, + era: 3 + } + ] + ); + + // When + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 40, 0)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 550, 0)); + assert_ok!(fully_unbond_permissioned(10)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 6 => UnbondPool { points: 2, balance: 2 }} + ); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 0, + roles: DEFAULT_ROLES, + state: PoolState::Destroying, + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); + + assert_eq!(Balances::free_balance(&550), 550 + 550 + 92); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 40, pool_id: 1, points: 6, balance: 6 }, + Event::MemberRemoved { pool_id: 1, member: 40 }, + Event::Withdrawn { member: 550, pool_id: 1, points: 92, balance: 92 }, + Event::MemberRemoved { pool_id: 1, member: 550 }, + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::Unbonded { member: 10, pool_id: 1, points: 2, balance: 2, era: 6 } + ] + ); + }); + } + + #[test] + fn unbond_merges_older_pools() { + ExtBuilder::default().with_check(1).build_and_execute(|| { + // Given + assert_eq!(StakingMock::bonding_duration(), 3); + SubPoolsStorage::::insert( + 1, + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { balance: 10, points: 100 }, + 1 + 3 => UnbondPool { balance: 20, points: 20 }, + 2 + 3 => UnbondPool { balance: 101, points: 101} + }, + }, + ); + unsafe_set_state(1, PoolState::Destroying); + + // When + let current_era = 1 + TotalUnbondingPools::::get(); + CurrentEra::set(current_era); + + assert_ok!(fully_unbond_permissioned(10)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: UnbondPool { balance: 10 + 20, points: 100 + 20 }, + with_era: unbonding_pools_with_era! { + 2 + 3 => UnbondPool { balance: 101, points: 101}, + current_era + 3 => UnbondPool { balance: 10, points: 10 }, + }, + }, + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 } + ] + ); + }); + } + + #[test] + fn unbond_kick_works() { + // Kick: the pool is blocked and the caller is either the root or bouncer. + ExtBuilder::default() + .add_members(vec![(100, 100), (200, 200)]) + .build_and_execute(|| { + // Given + unsafe_set_state(1, PoolState::Blocked); + let bonded_pool = BondedPool::::get(1).unwrap(); + assert_eq!(bonded_pool.roles.root.unwrap(), 900); + assert_eq!(bonded_pool.roles.nominator.unwrap(), 901); + assert_eq!(bonded_pool.roles.bouncer.unwrap(), 902); + + // When the nominator tries to kick, then its a noop + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(901), 100), + Error::::NotKickerOrDestroying + ); + + // When the root kicks then its ok + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(900), 100)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Bonded { member: 200, pool_id: 1, bonded: 200, joined: true }, + Event::Unbonded { + member: 100, + pool_id: 1, + points: 100, + balance: 100, + era: 3 + }, + ] + ); + + // When the bouncer kicks then its ok + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(902), 200)); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { + member: 200, + pool_id: 1, + points: 200, + balance: 200, + era: 3 + }] + ); + + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 3, + points: 10, // Only 10 points because 200 + 100 was unbonded + roles: DEFAULT_ROLES, + state: PoolState::Blocked, + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 10); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 100 + 200, balance: 100 + 200 } + }, + } + ); + assert_eq!( + *UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), + 100 + 200 + ); + }); + } + + #[test] + fn unbond_permissionless_works() { + // Scenarios where non-admin accounts can unbond others + ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { + // Given the pool is blocked + unsafe_set_state(1, PoolState::Blocked); + + // A permissionless unbond attempt errors + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(420), 100), + Error::::NotKickerOrDestroying + ); + + // permissionless unbond must be full + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(420), 100, 80), + Error::::PartialUnbondNotAllowedPermissionlessly, + ); + + // Given the pool is destroying + unsafe_set_state(1, PoolState::Destroying); + + // The depositor cannot be fully unbonded until they are the last member + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(10), 10), + Error::::MinimumBondNotMet, + ); + + // Any account can unbond a member that is not the depositor + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(420), 100)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 } + ] + ); + + // still permissionless unbond must be full + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(420), 100, 80), + Error::::PartialUnbondNotAllowedPermissionlessly, + ); + + // Given the pool is blocked + unsafe_set_state(1, PoolState::Blocked); + + // The depositor cannot be unbonded + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(420), 10), + Error::::DoesNotHavePermission + ); + + // Given the pools is destroying + unsafe_set_state(1, PoolState::Destroying); + + // The depositor cannot be unbonded yet. + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(420), 10), + Error::::DoesNotHavePermission, + ); + + // but when everyone is unbonded it can.. + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 100, 0)); + + // still permissionless unbond must be full. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(420), 10, 5), + Error::::PartialUnbondNotAllowedPermissionlessly, + ); + + // depositor can never be unbonded permissionlessly . + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(420), 10), + Error::::DoesNotHavePermission + ); + // but depositor itself can do it. + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(10), 10)); + + assert_eq!(BondedPools::::get(1).unwrap().points, 0); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 + 3 => UnbondPool { points: 10, balance: 10 } + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); + assert_eq!(*UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), 10); + }); + } + + #[test] + #[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] + #[cfg_attr(not(debug_assertions), should_panic)] + fn unbond_errors_correctly() { + ExtBuilder::default().build_and_execute(|| { + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(11), 11), + Error::::PoolMemberNotFound + ); + + // Add the member + let member = PoolMember { pool_id: 2, points: 10, ..Default::default() }; + PoolMembers::::insert(11, member); + + let _ = Pools::fully_unbond(RuntimeOrigin::signed(11), 11); + }); + } + + #[test] + #[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] + #[cfg_attr(not(debug_assertions), should_panic)] + fn unbond_panics_when_reward_pool_not_found() { + ExtBuilder::default().build_and_execute(|| { + let member = PoolMember { pool_id: 2, points: 10, ..Default::default() }; + PoolMembers::::insert(11, member); + BondedPool:: { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 10, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + + let _ = Pools::fully_unbond(RuntimeOrigin::signed(11), 11); + }); + } + + #[test] + fn partial_unbond_era_tracking() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // to make the depositor capable of withdrawing. + StakingMinBond::set(1); + MinCreateBond::::set(1); + MinJoinBond::::set(1); + assert_eq!(Pools::depositor_min_bond(), 1); + + // given + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 10); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 0); + assert_eq!(PoolMembers::::get(10).unwrap().pool_id, 1); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!() + ); + assert_eq!(BondedPool::::get(1).unwrap().points, 10); + assert!(SubPoolsStorage::::get(1).is_none()); + assert_eq!(CurrentEra::get(), 0); + assert_eq!(BondingDuration::get(), 3); + + // so the depositor can leave, just keeps the test simpler. + unsafe_set_state(1, PoolState::Destroying); + + // when: casual unbond + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 1)); + + // then + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 9); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 1); + assert_eq!(BondedPool::::get(1).unwrap().points, 9); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 1) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 3 } + ] + ); + + // when: casual further unbond, same era. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); + + // then + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 4); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 6); + assert_eq!(BondedPool::::get(1).unwrap().points, 4); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 6) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 3 }] + ); + + // when: casual further unbond, next era. + CurrentEra::set(1); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 1)); + + // then + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 3); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 7); + assert_eq!(BondedPool::::get(1).unwrap().points, 3); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 6, 4 => 1) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 }] + ); + + // when: unbonding more than our active: error + assert_noop!( + frame_support::storage::with_storage_layer(|| Pools::unbond( + RuntimeOrigin::signed(10), + 10, + 5 + )), + Error::::MinimumBondNotMet + ); + // instead: + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3)); + + // then + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 0); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 10); + assert_eq!(BondedPool::::get(1).unwrap().points, 0); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 6, 4 => 4) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 4, balance: 4 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 4 }] + ); + }); + } + + #[test] + fn partial_unbond_max_chunks() { + ExtBuilder::default().add_members(vec![(20, 20)]).ed(1).build_and_execute(|| { + MaxUnbonding::set(2); + + // given + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); + CurrentEra::set(1); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 3)); + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 2, 4 => 3) + ); + + // when + CurrentEra::set(2); + assert_noop!( + frame_support::storage::with_storage_layer(|| Pools::unbond( + RuntimeOrigin::signed(20), + 20, + 4 + )), + Error::::MaxUnbondingLimit + ); + + // when + MaxUnbonding::set(3); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 1)); + + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 2, 4 => 3, 5 => 1) + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Unbonded { member: 20, pool_id: 1, points: 2, balance: 2, era: 3 }, + Event::Unbonded { member: 20, pool_id: 1, points: 3, balance: 3, era: 4 }, + Event::Unbonded { member: 20, pool_id: 1, points: 1, balance: 1, era: 5 } + ] + ); + }) + } + + // depositor can unbond only up to `MinCreateBond`. + #[test] + fn depositor_permissioned_partial_unbond() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // given + StakingMinBond::set(5); + assert_eq!(Pools::depositor_min_bond(), 5); + + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 10); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 0); + + // can unbond a bit.. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3)); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 7); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 3); + + // but not less than 2 + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 6), + Error::::MinimumBondNotMet + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 3 } + ] + ); + }); + } + + #[test] + fn depositor_permissioned_partial_unbond_slashed() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // given + assert_eq!(MinCreateBond::::get(), 2); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 10); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 0); + + // slash the default pool + StakingMock::set_bonded_balance(Pools::create_bonded_account(1), 5); + + // cannot unbond even 7, because the value of shares is now less. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 7), + Error::::MinimumBondNotMet + ); + }); + } + + #[test] + fn every_unbonding_triggers_payout() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + let initial_reward_account = Balances::free_balance(default_reward_account()); + assert_eq!(initial_reward_account, Balances::minimum_balance()); + assert_eq!(initial_reward_account, 5); + + Balances::make_free_balance_be( + &default_reward_account(), + 4 * Balances::minimum_balance(), + ); + + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); + assert_eq!( + pool_events_since_last_call(), + vec![ + // 2/3 of ed, which is 20's share. + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, + Event::Unbonded { member: 20, pool_id: 1, balance: 2, points: 2, era: 3 } + ] + ); + + CurrentEra::set(1); + Balances::make_free_balance_be( + &default_reward_account(), + 4 * Balances::minimum_balance(), + ); + + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 3)); + assert_eq!( + pool_events_since_last_call(), + vec![ + // 2/3 of ed, which is 20's share. + Event::PaidOut { member: 20, pool_id: 1, payout: 6 }, + Event::Unbonded { member: 20, pool_id: 1, points: 3, balance: 3, era: 4 } + ] + ); + + CurrentEra::set(2); + Balances::make_free_balance_be( + &default_reward_account(), + 4 * Balances::minimum_balance(), + ); + + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 20, pool_id: 1, payout: 3 }, + Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 } + ] + ); + + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 2, 4 => 3, 5 => 5) + ); + }); + } +} + +mod pool_withdraw_unbonded { + use super::*; + + #[test] + fn pool_withdraw_unbonded_works() { + ExtBuilder::default().build_and_execute(|| { + // Given 10 unbond'ed directly against the pool account + assert_ok!(StakingMock::unbond(&default_bonded_account(), 5)); + // and the pool account only has 10 balance + assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(5)); + assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(10)); + assert_eq!(Balances::free_balance(&default_bonded_account()), 10); + + // When + assert_ok!(Pools::pool_withdraw_unbonded(RuntimeOrigin::signed(10), 1, 0)); + + // Then there unbonding balance is no longer locked + assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(5)); + assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(5)); + assert_eq!(Balances::free_balance(&default_bonded_account()), 10); + }); + } +} + +mod withdraw_unbonded { + use super::*; + use frame_support::bounded_btree_map; + + #[test] + fn withdraw_unbonded_works_against_slashed_no_era_sub_pool() { + ExtBuilder::default() + .add_members(vec![(40, 40), (550, 550)]) + .build_and_execute(|| { + // reduce the noise a bit. + let _ = balances_events_since_last_call(); + + // Given + assert_eq!(StakingMock::bonding_duration(), 3); + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(550), 550)); + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(40), 40)); + assert_eq!(Balances::free_balance(&default_bonded_account()), 600); + + let mut current_era = 1; + CurrentEra::set(current_era); + + let mut sub_pools = SubPoolsStorage::::get(1).unwrap(); + let unbond_pool = sub_pools.with_era.get_mut(&3).unwrap(); + // Sanity check + assert_eq!(*unbond_pool, UnbondPool { points: 550 + 40, balance: 550 + 40 }); + + // Simulate a slash to the pool with_era(current_era), decreasing the balance by + // half + { + unbond_pool.balance /= 2; // 295 + SubPoolsStorage::::insert(1, sub_pools); + // Update the equivalent of the unbonding chunks for the `StakingMock` + let mut x = UnbondingBalanceMap::get(); + *x.get_mut(&default_bonded_account()).unwrap() /= 5; + UnbondingBalanceMap::set(&x); + Balances::make_free_balance_be( + &default_bonded_account(), + Balances::free_balance(&default_bonded_account()) / 2, // 300 + ); + StakingMock::set_bonded_balance( + default_bonded_account(), + StakingMock::active_stake(&default_bonded_account()).unwrap() / 2, + ); + }; + + // Advance the current_era to ensure all `with_era` pools will be merged into + // `no_era` pool + current_era += TotalUnbondingPools::::get(); + CurrentEra::set(current_era); + + // Simulate some other call to unbond that would merge `with_era` pools into + // `no_era` + let sub_pools = + SubPoolsStorage::::get(1).unwrap().maybe_merge_pools(current_era); + SubPoolsStorage::::insert(1, sub_pools); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: UnbondPool { points: 550 + 40, balance: 275 + 20 }, + with_era: Default::default() + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, + Event::Unbonded { + member: 550, + pool_id: 1, + points: 550, + balance: 550, + era: 3 + }, + Event::Unbonded { member: 40, pool_id: 1, points: 40, balance: 40, era: 3 }, + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::BalanceSet { who: default_bonded_account(), free: 300 }] + ); + + // When + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(550), 550, 0)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().no_era, + UnbondPool { points: 40, balance: 20 } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 550, pool_id: 1, balance: 275, points: 550 }, + Event::MemberRemoved { pool_id: 1, member: 550 } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 550, amount: 275 }] + ); + + // When + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(40), 40, 0)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().no_era, + UnbondPool { points: 0, balance: 0 } + ); + assert!(!PoolMembers::::contains_key(40)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 40, pool_id: 1, balance: 20, points: 40 }, + Event::MemberRemoved { pool_id: 1, member: 40 } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 40, amount: 20 }] + ); + + // now, finally, the depositor can take out its share. + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(10)); + + current_era += 3; + CurrentEra::set(current_era); + + // when + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Unbonded { member: 10, pool_id: 1, balance: 5, points: 5, era: 9 }, + Event::Withdrawn { member: 10, pool_id: 1, balance: 5, points: 5 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 } + ] + ); + assert!(!Metadata::::contains_key(1)); + assert_eq!( + balances_events_since_last_call(), + vec![ + BEvent::Transfer { from: default_bonded_account(), to: 10, amount: 5 }, + BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } + ] + ); + }); + } + + #[test] + fn withdraw_unbonded_works_against_slashed_with_era_sub_pools() { + ExtBuilder::default() + .add_members(vec![(40, 40), (550, 550)]) + .build_and_execute(|| { + let _ = balances_events_since_last_call(); + + // Given + // current bond is 600, we slash it all to 300. + StakingMock::set_bonded_balance(default_bonded_account(), 300); + Balances::make_free_balance_be(&default_bonded_account(), 300); + assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(300)); + + assert_ok!(fully_unbond_permissioned(40)); + assert_ok!(fully_unbond_permissioned(550)); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 550 / 2 + 40 / 2, balance: 550 / 2 + 40 / 2 + }} + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, + Event::Unbonded { member: 40, pool_id: 1, balance: 20, points: 20, era: 3 }, + Event::Unbonded { + member: 550, + pool_id: 1, + balance: 275, + points: 275, + era: 3, + } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::BalanceSet { who: default_bonded_account(), free: 300 },] + ); + + CurrentEra::set(StakingMock::bonding_duration()); + + // When + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(40), 40, 0)); + + // Then + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 40, amount: 20 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 40, pool_id: 1, balance: 20, points: 20 }, + Event::MemberRemoved { pool_id: 1, member: 40 } + ] + ); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 550 / 2, balance: 550 / 2 }} + ); + + // When + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(550), 550, 0)); + + // Then + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 550, amount: 275 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 550, pool_id: 1, balance: 275, points: 275 }, + Event::MemberRemoved { pool_id: 1, member: 550 } + ] + ); + assert!(SubPoolsStorage::::get(1).unwrap().with_era.is_empty()); + + // now, finally, the depositor can take out its share. + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(10)); + + // because everyone else has left, the points + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 6 => UnbondPool { points: 5, balance: 5 }} + ); + + CurrentEra::set(CurrentEra::get() + 3); + + // set metadata to check that it's being removed on dissolve + assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); + assert!(Metadata::::contains_key(1)); + + // when + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // then + assert_eq!(Balances::free_balance(&10), 10 + 35); + assert_eq!(Balances::free_balance(&default_bonded_account()), 0); + + // in this test 10 also gets a fair share of the slash, because the slash was + // applied to the bonded account. + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 6 }, + Event::Withdrawn { member: 10, pool_id: 1, points: 5, balance: 5 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 } + ] + ); + assert!(!Metadata::::contains_key(1)); + assert_eq!( + balances_events_since_last_call(), + vec![ + BEvent::Transfer { from: default_bonded_account(), to: 10, amount: 5 }, + BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } + ] + ); + }); + } + + #[test] + fn withdraw_unbonded_handles_faulty_sub_pool_accounting() { + ExtBuilder::default().build_and_execute(|| { + // Given + assert_eq!(Balances::minimum_balance(), 5); + assert_eq!(Balances::free_balance(&10), 35); + assert_eq!(Balances::free_balance(&default_bonded_account()), 10); + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(10), 10)); + + // Simulate a slash that is not accounted for in the sub pools. + Balances::make_free_balance_be(&default_bonded_account(), 5); + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + //------------------------------balance decrease is not account for + unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 } } + ); + + CurrentEra::set(3); + + // When + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // Then + assert_eq!(Balances::free_balance(10), 10 + 35); + assert_eq!(Balances::free_balance(&default_bonded_account()), 0); + }); + } + + #[test] + fn withdraw_unbonded_errors_correctly() { + ExtBuilder::default().with_check(0).build_and_execute(|| { + // Insert the sub-pool + let sub_pools = SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 }}, + }; + SubPoolsStorage::::insert(1, sub_pools.clone()); + + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), + Error::::PoolMemberNotFound + ); + + let mut member = PoolMember { pool_id: 1, points: 10, ..Default::default() }; + PoolMembers::::insert(11, member.clone()); + + // Simulate calling `unbond` + member.unbonding_eras = member_unbonding_eras!(3 => 10); + PoolMembers::::insert(11, member.clone()); + + // We are still in the bonding duration + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), + Error::::CannotWithdrawAny + ); + + // If we error the member does not get removed + assert_eq!(PoolMembers::::get(11), Some(member)); + // and the sub pools do not get updated. + assert_eq!(SubPoolsStorage::::get(1).unwrap(), sub_pools) + }); + } + + #[test] + fn withdraw_unbonded_kick() { + ExtBuilder::default() + .add_members(vec![(100, 100), (200, 200)]) + .build_and_execute(|| { + // Given + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(100), 100)); + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(200), 200)); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 3, + points: 10, + roles: DEFAULT_ROLES, + state: PoolState::Open, + } + } + ); + CurrentEra::set(StakingMock::bonding_duration()); + + // Cannot kick when pool is open + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(902), 100, 0), + Error::::NotKickerOrDestroying + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Bonded { member: 200, pool_id: 1, bonded: 200, joined: true }, + Event::Unbonded { + member: 100, + pool_id: 1, + points: 100, + balance: 100, + era: 3 + }, + Event::Unbonded { + member: 200, + pool_id: 1, + points: 200, + balance: 200, + era: 3 + } + ] + ); + + // Given + unsafe_set_state(1, PoolState::Blocked); + + // Cannot kick as a nominator + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(901), 100, 0), + Error::::NotKickerOrDestroying + ); + + // Can kick as root + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(900), 100, 0)); + + // Can kick as bouncer + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(900), 200, 0)); + + assert_eq!(Balances::free_balance(100), 100 + 100); + assert_eq!(Balances::free_balance(200), 200 + 200); + assert!(!PoolMembers::::contains_key(100)); + assert!(!PoolMembers::::contains_key(200)); + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 100, pool_id: 1, points: 100, balance: 100 }, + Event::MemberRemoved { pool_id: 1, member: 100 }, + Event::Withdrawn { member: 200, pool_id: 1, points: 200, balance: 200 }, + Event::MemberRemoved { pool_id: 1, member: 200 } + ] + ); + }); + } + + #[test] + fn withdraw_unbonded_destroying_permissionless() { + ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { + // Given + assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(100), 100)); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 2, + points: 10, + roles: DEFAULT_ROLES, + state: PoolState::Open, + } + } + ); + CurrentEra::set(StakingMock::bonding_duration()); + assert_eq!(Balances::free_balance(100), 100); + + // Cannot permissionlessly withdraw + assert_noop!( + Pools::fully_unbond(RuntimeOrigin::signed(420), 100), + Error::::NotKickerOrDestroying + ); + + // Given + unsafe_set_state(1, PoolState::Destroying); + + // Can permissionlesly withdraw a member that is not the depositor + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(420), 100, 0)); + + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default(),); + assert_eq!(Balances::free_balance(100), 100 + 100); + assert!(!PoolMembers::::contains_key(100)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, + Event::Withdrawn { member: 100, pool_id: 1, points: 100, balance: 100 }, + Event::MemberRemoved { pool_id: 1, member: 100 } + ] + ); + }); + } + + #[test] + fn partial_withdraw_unbonded_depositor() { + ExtBuilder::default().ed(1).build_and_execute(|| { + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + unsafe_set_state(1, PoolState::Destroying); + + // given + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 6)); + CurrentEra::set(1); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 1)); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 6, 4 => 1) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 13); + assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 7); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, + Event::Unbonded { member: 10, pool_id: 1, points: 6, balance: 6, era: 3 }, + Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 } + ] + ); + + // when + CurrentEra::set(2); + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0), + Error::::CannotWithdrawAny + ); + + // when + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // then + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(4 => 1) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 10, pool_id: 1, points: 6, balance: 6 }] + ); + + // when + CurrentEra::set(4); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // then + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!() + ); + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 10, pool_id: 1, points: 1, balance: 1 },] + ); + + // when repeating: + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0), + Error::::CannotWithdrawAny + ); + }); + } + + #[test] + fn partial_withdraw_unbonded_non_depositor() { + ExtBuilder::default().add_members(vec![(11, 10)]).build_and_execute(|| { + // given + assert_ok!(Pools::unbond(RuntimeOrigin::signed(11), 11, 6)); + CurrentEra::set(1); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(11), 11, 1)); + assert_eq!( + PoolMembers::::get(11).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 6, 4 => 1) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!(PoolMembers::::get(11).unwrap().active_points(), 3); + assert_eq!(PoolMembers::::get(11).unwrap().unbonding_points(), 7); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 11, pool_id: 1, points: 6, balance: 6, era: 3 }, + Event::Unbonded { member: 11, pool_id: 1, points: 1, balance: 1, era: 4 } + ] + ); + + // when + CurrentEra::set(2); + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), + Error::::CannotWithdrawAny + ); + + // when + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0)); + + // then + assert_eq!( + PoolMembers::::get(11).unwrap().unbonding_eras, + member_unbonding_eras!(4 => 1) + ); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 11, pool_id: 1, points: 6, balance: 6 }] + ); + + // when + CurrentEra::set(4); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0)); + + // then + assert_eq!( + PoolMembers::::get(11).unwrap().unbonding_eras, + member_unbonding_eras!() + ); + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 11, pool_id: 1, points: 1, balance: 1 }] + ); + + // when repeating: + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), + Error::::CannotWithdrawAny + ); + }); + } + + #[test] + fn full_multi_step_withdrawing_non_depositor() { + ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { + // given + assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 75)); + assert_eq!( + PoolMembers::::get(100).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 75) + ); + + // progress one era and unbond the leftover. + CurrentEra::set(1); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 25)); + assert_eq!( + PoolMembers::::get(100).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 75, 4 => 25) + ); + + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0), + Error::::CannotWithdrawAny + ); + + // now the 75 should be free. + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 75, balance: 75, era: 3 }, + Event::Unbonded { member: 100, pool_id: 1, points: 25, balance: 25, era: 4 }, + Event::Withdrawn { member: 100, pool_id: 1, points: 75, balance: 75 }, + ] + ); + assert_eq!( + PoolMembers::::get(100).unwrap().unbonding_eras, + member_unbonding_eras!(4 => 25) + ); + + // the 25 should be free now, and the member removed. + CurrentEra::set(4); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 100, pool_id: 1, points: 25, balance: 25 }, + Event::MemberRemoved { pool_id: 1, member: 100 } + ] + ); + }) + } + + #[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(RuntimeOrigin::signed(20), 20, 5)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(30), 30, 5)); + + // then member-local unbonding is pretty much in sync with the global pools. + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 5) + ); + assert_eq!( + PoolMembers::::get(30).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 5) + ); + assert_eq!( + SubPoolsStorage::::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(RuntimeOrigin::signed(20), 20, 5)); + + // then still member-local unbonding is pretty much in sync with the global pools. + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 5, 4 => 5) + ); + assert_eq!( + SubPoolsStorage::::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(RuntimeOrigin::signed(20), 20, 5)); + + // then still member-local unbonding is pretty much in sync with the global pools. + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5) + ); + assert_eq!( + SubPoolsStorage::::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(RuntimeOrigin::signed(20), 20, 5)); + + // then + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5, 8 => 5) + ); + assert_eq!( + SubPoolsStorage::::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(RuntimeOrigin::signed(20), 20, 0)); + // then + assert_eq!( + PoolMembers::::get(20).unwrap().unbonding_eras, + member_unbonding_eras!(8 => 5) + ); + assert_eq!( + SubPoolsStorage::::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(RuntimeOrigin::signed(30), 30, 0)); + // then + assert_eq!( + PoolMembers::::get(30).unwrap().unbonding_eras, + member_unbonding_eras!() + ); + assert_eq!( + SubPoolsStorage::::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(|| { + // depositor now has 20, they can unbond to 10. + assert_eq!(Pools::depositor_min_bond(), 10); + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + + // now they can. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 7)); + + // progress one era and unbond the leftover. + CurrentEra::set(1); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3)); + + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(3 => 7, 4 => 3) + ); + + // they can't unbond to a value below 10 other than 0.. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 5), + Error::::MinimumBondNotMet + ); + + // but not even full, because they pool is not yet destroying. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 10), + Error::::MinimumBondNotMet + ); + + // but now they can. + unsafe_set_state(1, PoolState::Destroying); + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 5), + Error::::MinimumBondNotMet + ); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10)); + + // now the 7 should be free. + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, + Event::Unbonded { member: 10, pool_id: 1, balance: 7, points: 7, era: 3 }, + Event::Unbonded { member: 10, pool_id: 1, balance: 3, points: 3, era: 4 }, + Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 }, + Event::Withdrawn { member: 10, pool_id: 1, balance: 7, points: 7 } + ] + ); + assert_eq!( + PoolMembers::::get(10).unwrap().unbonding_eras, + member_unbonding_eras!(4 => 13) + ); + + // the 13 should be free now, and the member removed. + CurrentEra::set(4); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 10, pool_id: 1, points: 13, balance: 13 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 }, + ] + ); + assert!(!Metadata::::contains_key(1)); + }) + } + + #[test] + fn withdraw_unbonded_removes_claim_permissions_on_leave() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + // Given + CurrentEra::set(1); + assert_eq!(PoolMembers::::get(20).unwrap().points, 20); + + assert_ok!(Pools::set_claim_permission( + RuntimeOrigin::signed(20), + ClaimPermission::PermissionlessAll + )); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 20)); + assert_eq!(ClaimPermissions::::get(20), ClaimPermission::PermissionlessAll); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 4 }, + ] + ); + + CurrentEra::set(5); + + // When + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 20, pool_id: 1, balance: 20, points: 20 }, + Event::MemberRemoved { pool_id: 1, member: 20 } + ] + ); + + // Then + assert_eq!(PoolMembers::::get(20), None); + assert_eq!(ClaimPermissions::::contains_key(20), false); + }); + } +} + +mod create { + use super::*; + + #[test] + fn create_works() { + ExtBuilder::default().build_and_execute(|| { + // next pool id is 2. + let next_pool_stash = Pools::create_bonded_account(2); + let ed = Balances::minimum_balance(); + + assert!(!BondedPools::::contains_key(2)); + assert!(!RewardPools::::contains_key(2)); + assert!(!PoolMembers::::contains_key(11)); + assert_err!(StakingMock::active_stake(&next_pool_stash), "balance not found"); + + Balances::make_free_balance_be(&11, StakingMock::minimum_nominator_bond() + ed); + assert_ok!(Pools::create( + RuntimeOrigin::signed(11), + StakingMock::minimum_nominator_bond(), + 123, + 456, + 789 + )); + + assert_eq!(Balances::free_balance(&11), 0); + assert_eq!( + PoolMembers::::get(11).unwrap(), + PoolMember { + pool_id: 2, + points: StakingMock::minimum_nominator_bond(), + ..Default::default() + } + ); + assert_eq!( + BondedPool::::get(2).unwrap(), + BondedPool { + id: 2, + inner: BondedPoolInner { + commission: Commission::default(), + points: StakingMock::minimum_nominator_bond(), + member_counter: 1, + roles: PoolRoles { + depositor: 11, + root: Some(123), + nominator: Some(456), + bouncer: Some(789) + }, + state: PoolState::Open, + } + } + ); + assert_eq!( + StakingMock::active_stake(&next_pool_stash).unwrap(), + StakingMock::minimum_nominator_bond() + ); + assert_eq!( + RewardPools::::get(2).unwrap(), + RewardPool { ..Default::default() } + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Created { depositor: 11, pool_id: 2 }, + Event::Bonded { member: 11, pool_id: 2, bonded: 10, joined: true } + ] + ); + }); + } + + #[test] + fn create_errors_correctly() { + ExtBuilder::default().with_check(0).build_and_execute(|| { + assert_noop!( + Pools::create(RuntimeOrigin::signed(10), 420, 123, 456, 789), + Error::::AccountBelongsToOtherPool + ); + + // Given + assert_eq!(MinCreateBond::::get(), 2); + assert_eq!(StakingMock::minimum_nominator_bond(), 10); + + // Then + assert_noop!( + Pools::create(RuntimeOrigin::signed(11), 9, 123, 456, 789), + Error::::MinimumBondNotMet + ); + + // Given + MinCreateBond::::put(20); + + // Then + assert_noop!( + Pools::create(RuntimeOrigin::signed(11), 19, 123, 456, 789), + Error::::MinimumBondNotMet + ); + + // Given + BondedPool:: { + id: 2, + inner: BondedPoolInner { + commission: Commission::default(), + member_counter: 1, + points: 10, + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + assert_eq!(MaxPools::::get(), Some(2)); + assert_eq!(BondedPools::::count(), 2); + + // Then + assert_noop!( + Pools::create(RuntimeOrigin::signed(11), 20, 123, 456, 789), + Error::::MaxPools + ); + + // Given + assert_eq!(PoolMembers::::count(), 1); + MaxPools::::put(3); + MaxPoolMembers::::put(1); + Balances::make_free_balance_be(&11, 5 + 20); + + // Then + let create = RuntimeCall::Pools(crate::Call::::create { + amount: 20, + root: 11, + nominator: 11, + bouncer: 11, + }); + assert_noop!( + create.dispatch(RuntimeOrigin::signed(11)), + Error::::MaxPoolMembers + ); + }); + } + + #[test] + fn create_with_pool_id_works() { + ExtBuilder::default().build_and_execute(|| { + let ed = Balances::minimum_balance(); + + Balances::make_free_balance_be(&11, StakingMock::minimum_nominator_bond() + ed); + assert_ok!(Pools::create( + RuntimeOrigin::signed(11), + StakingMock::minimum_nominator_bond(), + 123, + 456, + 789 + )); + + assert_eq!(Balances::free_balance(&11), 0); + // delete the initial pool created, then pool_Id `1` will be free + + assert_noop!( + Pools::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 1), + Error::::PoolIdInUse + ); + + assert_noop!( + Pools::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 3), + Error::::InvalidPoolId + ); + + // start dismantling the pool. + assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); + assert_ok!(fully_unbond_permissioned(10)); + + CurrentEra::set(3); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 10)); + + assert_ok!(Pools::create_with_pool_id(RuntimeOrigin::signed(10), 20, 234, 654, 783, 1)); + }); + } +} + +#[test] +fn set_claimable_actor_works() { + ExtBuilder::default().build_and_execute(|| { + // Given + Balances::make_free_balance_be(&11, ExistentialDeposit::get() + 2); + assert!(!PoolMembers::::contains_key(11)); + + // When + assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1)); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, + ] + ); + + // Make permissionless + assert_eq!(ClaimPermissions::::get(11), ClaimPermission::Permissioned); + assert_noop!( + Pools::set_claim_permission( + RuntimeOrigin::signed(12), + ClaimPermission::PermissionlessAll + ), + Error::::PoolMemberNotFound + ); + assert_ok!(Pools::set_claim_permission( + RuntimeOrigin::signed(11), + ClaimPermission::PermissionlessAll + )); + + // then + assert_eq!(ClaimPermissions::::get(11), ClaimPermission::PermissionlessAll); + }); +} + +mod nominate { + use super::*; + + #[test] + fn nominate_works() { + ExtBuilder::default().build_and_execute(|| { + // Depositor can't nominate + assert_noop!( + Pools::nominate(RuntimeOrigin::signed(10), 1, vec![21]), + Error::::NotNominator + ); + + // bouncer can't nominate + assert_noop!( + Pools::nominate(RuntimeOrigin::signed(902), 1, vec![21]), + Error::::NotNominator + ); + + // Root can nominate + assert_ok!(Pools::nominate(RuntimeOrigin::signed(900), 1, vec![21])); + assert_eq!(Nominations::get().unwrap(), vec![21]); + + // Nominator can nominate + assert_ok!(Pools::nominate(RuntimeOrigin::signed(901), 1, vec![31])); + assert_eq!(Nominations::get().unwrap(), vec![31]); + + // Can't nominate for a pool that doesn't exist + assert_noop!( + Pools::nominate(RuntimeOrigin::signed(902), 123, vec![21]), + Error::::PoolNotFound + ); + }); + } +} + +mod set_state { + use super::*; + + #[test] + fn set_state_works() { + ExtBuilder::default().build_and_execute(|| { + // Given + assert_ok!(BondedPool::::get(1).unwrap().ok_to_be_open()); + + // Only the root and bouncer can change the state when the pool is ok to be open. + assert_noop!( + Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + assert_noop!( + Pools::set_state(RuntimeOrigin::signed(901), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + + // Root can change state + assert_ok!(Pools::set_state(RuntimeOrigin::signed(900), 1, PoolState::Blocked)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::StateChanged { pool_id: 1, new_state: PoolState::Blocked } + ] + ); + + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Blocked); + + // bouncer can change state + assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); + + // If the pool is destroying, then no one can set state + assert_noop!( + Pools::set_state(RuntimeOrigin::signed(900), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + assert_noop!( + Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + + // If the pool is not ok to be open, then anyone can set it to destroying + + // Given + unsafe_set_state(1, PoolState::Open); + let mut bonded_pool = BondedPool::::get(1).unwrap(); + bonded_pool.points = 100; + bonded_pool.put(); + // When + assert_ok!(Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying)); + // Then + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); + + // Given + Balances::make_free_balance_be(&default_bonded_account(), Balance::max_value() / 10); + unsafe_set_state(1, PoolState::Open); + // When + assert_ok!(Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying)); + // Then + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); + + // If the pool is not ok to be open, it cannot be permissionlessly set to a state that + // isn't destroying + unsafe_set_state(1, PoolState::Open); + assert_noop!( + Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying } + ] + ); + }); + } +} + +mod set_metadata { + use super::*; + + #[test] + fn set_metadata_works() { + ExtBuilder::default().build_and_execute(|| { + // Root can set metadata + assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); + assert_eq!(Metadata::::get(1), vec![1, 1]); + + // bouncer can set metadata + assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(902), 1, vec![2, 2])); + assert_eq!(Metadata::::get(1), vec![2, 2]); + + // Depositor can't set metadata + assert_noop!( + Pools::set_metadata(RuntimeOrigin::signed(10), 1, vec![3, 3]), + Error::::DoesNotHavePermission + ); + + // Nominator can't set metadata + assert_noop!( + Pools::set_metadata(RuntimeOrigin::signed(901), 1, vec![3, 3]), + Error::::DoesNotHavePermission + ); + + // Metadata cannot be longer than `MaxMetadataLen` + assert_noop!( + Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1, 1]), + Error::::MetadataExceedsMaxLen + ); + }); + } +} + +mod set_configs { + use super::*; + + #[test] + fn set_configs_works() { + ExtBuilder::default().build_and_execute(|| { + // Setting works + assert_ok!(Pools::set_configs( + RuntimeOrigin::root(), + ConfigOp::Set(1 as Balance), + ConfigOp::Set(2 as Balance), + ConfigOp::Set(3u32), + ConfigOp::Set(4u32), + ConfigOp::Set(5u32), + ConfigOp::Set(Perbill::from_percent(6)) + )); + assert_eq!(MinJoinBond::::get(), 1); + assert_eq!(MinCreateBond::::get(), 2); + assert_eq!(MaxPools::::get(), Some(3)); + assert_eq!(MaxPoolMembers::::get(), Some(4)); + assert_eq!(MaxPoolMembersPerPool::::get(), Some(5)); + assert_eq!(GlobalMaxCommission::::get(), Some(Perbill::from_percent(6))); + + // Noop does nothing + assert_storage_noop!(assert_ok!(Pools::set_configs( + RuntimeOrigin::root(), + ConfigOp::Noop, + ConfigOp::Noop, + ConfigOp::Noop, + ConfigOp::Noop, + ConfigOp::Noop, + ConfigOp::Noop, + ))); + + // Removing works + assert_ok!(Pools::set_configs( + RuntimeOrigin::root(), + ConfigOp::Remove, + ConfigOp::Remove, + ConfigOp::Remove, + ConfigOp::Remove, + ConfigOp::Remove, + ConfigOp::Remove, + )); + assert_eq!(MinJoinBond::::get(), 0); + assert_eq!(MinCreateBond::::get(), 0); + assert_eq!(MaxPools::::get(), None); + assert_eq!(MaxPoolMembers::::get(), None); + assert_eq!(MaxPoolMembersPerPool::::get(), None); + assert_eq!(GlobalMaxCommission::::get(), None); + }); + } +} + +mod bond_extra { + use super::*; + use crate::Event; + + #[test] + fn bond_extra_from_free_balance_creator() { + ExtBuilder::default().build_and_execute(|| { + // 10 is the owner and a member in pool 1, give them some more funds. + Balances::make_free_balance_be(&10, 100); + + // given + assert_eq!(PoolMembers::::get(10).unwrap().points, 10); + assert_eq!(BondedPools::::get(1).unwrap().points, 10); + assert_eq!(Balances::free_balance(10), 100); + + // when + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + + // then + assert_eq!(Balances::free_balance(10), 90); + assert_eq!(PoolMembers::::get(10).unwrap().points, 20); + assert_eq!(BondedPools::::get(1).unwrap().points, 20); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false } + ] + ); + + // when + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(20))); + + // then + assert_eq!(Balances::free_balance(10), 70); + assert_eq!(PoolMembers::::get(10).unwrap().points, 40); + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::Bonded { member: 10, pool_id: 1, bonded: 20, joined: false }] + ); + }) + } + + #[test] + fn bond_extra_from_rewards_creator() { + ExtBuilder::default().build_and_execute(|| { + // put some money in the reward account, all of which will belong to 10 as the only + // member of the pool. + Balances::make_free_balance_be(&default_reward_account(), 7); + // ... if which only 2 is claimable to make sure the reward account does not die. + let claimable_reward = 7 - ExistentialDeposit::get(); + + // given + assert_eq!(PoolMembers::::get(10).unwrap().points, 10); + assert_eq!(BondedPools::::get(1).unwrap().points, 10); + assert_eq!(Balances::free_balance(10), 35); + + // when + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::Rewards)); + + // then + assert_eq!(Balances::free_balance(10), 35); + assert_eq!(PoolMembers::::get(10).unwrap().points, 10 + claimable_reward); + assert_eq!(BondedPools::::get(1).unwrap().points, 10 + claimable_reward); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: claimable_reward }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: claimable_reward, + joined: false + } + ] + ); + }) + } + + #[test] + fn bond_extra_from_rewards_joiner() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + // put some money in the reward account, all of which will belong to 10 as the only + // member of the pool. + Balances::make_free_balance_be(&default_reward_account(), 8); + // ... if which only 3 is claimable to make sure the reward account does not die. + let claimable_reward = 8 - ExistentialDeposit::get(); + // NOTE: easier to read of we use 3, so let's use the number instead of variable. + assert_eq!(claimable_reward, 3, "test is correct if rewards are divisible by 3"); + + // given + assert_eq!(PoolMembers::::get(10).unwrap().points, 10); + assert_eq!(PoolMembers::::get(20).unwrap().points, 20); + assert_eq!(BondedPools::::get(1).unwrap().points, 30); + assert_eq!(Balances::free_balance(10), 35); + assert_eq!(Balances::free_balance(20), 20); + + // when + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::Rewards)); + assert_eq!(Balances::free_balance(&default_reward_account()), 7); + + // then + assert_eq!(Balances::free_balance(10), 35); + // 10's share of the reward is 1/3, since they gave 10/30 of the total shares. + assert_eq!(PoolMembers::::get(10).unwrap().points, 10 + 1); + assert_eq!(BondedPools::::get(1).unwrap().points, 30 + 1); + + // when + assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(20), BondExtra::Rewards)); + + // then + assert_eq!(Balances::free_balance(20), 20); + // 20's share of the rewards is the other 2/3 of the rewards, since they have 20/30 of + // the shares + assert_eq!(PoolMembers::::get(20).unwrap().points, 20 + 2); + assert_eq!(BondedPools::::get(1).unwrap().points, 30 + 3); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 1, joined: false }, + Event::PaidOut { member: 20, pool_id: 1, payout: 2 }, + Event::Bonded { member: 20, pool_id: 1, bonded: 2, joined: false } + ] + ); + }) + } + + #[test] + fn bond_extra_other() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + Balances::make_free_balance_be(&default_reward_account(), 8); + // ... of which only 3 are claimable to make sure the reward account does not die. + let claimable_reward = 8 - ExistentialDeposit::get(); + // NOTE: easier to read if we use 3, so let's use the number instead of variable. + assert_eq!(claimable_reward, 3, "test is correct if rewards are divisible by 3"); + + // given + assert_eq!(PoolMembers::::get(10).unwrap().points, 10); + assert_eq!(PoolMembers::::get(20).unwrap().points, 20); + assert_eq!(BondedPools::::get(1).unwrap().points, 30); + assert_eq!(Balances::free_balance(10), 35); + assert_eq!(Balances::free_balance(20), 20); + + // Permissioned by default + assert_noop!( + Pools::bond_extra_other(RuntimeOrigin::signed(80), 20, BondExtra::Rewards), + Error::::DoesNotHavePermission + ); + + assert_ok!(Pools::set_claim_permission( + RuntimeOrigin::signed(10), + ClaimPermission::PermissionlessAll + )); + assert_ok!(Pools::bond_extra_other(RuntimeOrigin::signed(50), 10, BondExtra::Rewards)); + assert_eq!(Balances::free_balance(&default_reward_account()), 7); + + // then + assert_eq!(Balances::free_balance(10), 35); + assert_eq!(PoolMembers::::get(10).unwrap().points, 10 + 1); + assert_eq!(BondedPools::::get(1).unwrap().points, 30 + 1); + + // when + assert_noop!( + Pools::bond_extra_other(RuntimeOrigin::signed(40), 40, BondExtra::Rewards), + Error::::PoolMemberNotFound + ); + + // when + assert_ok!(Pools::bond_extra_other( + RuntimeOrigin::signed(20), + 20, + BondExtra::FreeBalance(10) + )); + + // then + assert_eq!(Balances::free_balance(20), 12); + assert_eq!(Balances::free_balance(&default_reward_account()), 5); + assert_eq!(PoolMembers::::get(20).unwrap().points, 30); + assert_eq!(BondedPools::::get(1).unwrap().points, 41); + }) + } +} + +mod update_roles { + use super::*; + + #[test] + fn update_roles_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { + depositor: 10, + root: Some(900), + nominator: Some(901), + bouncer: Some(902) + }, + ); + + // non-existent pools + assert_noop!( + Pools::update_roles( + RuntimeOrigin::signed(1), + 2, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::PoolNotFound, + ); + + // depositor cannot change roles. + assert_noop!( + Pools::update_roles( + RuntimeOrigin::signed(1), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::DoesNotHavePermission, + ); + + // nominator cannot change roles. + assert_noop!( + Pools::update_roles( + RuntimeOrigin::signed(901), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::DoesNotHavePermission, + ); + // bouncer + assert_noop!( + Pools::update_roles( + RuntimeOrigin::signed(902), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::DoesNotHavePermission, + ); + + // but root can + assert_ok!(Pools::update_roles( + RuntimeOrigin::signed(900), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + )); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::RolesUpdated { root: Some(5), bouncer: Some(7), nominator: Some(6) } + ] + ); + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(5), nominator: Some(6), bouncer: Some(7) }, + ); + + // also root origin can + assert_ok!(Pools::update_roles( + RuntimeOrigin::root(), + 1, + ConfigOp::Set(1), + ConfigOp::Set(2), + ConfigOp::Set(3) + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { root: Some(1), bouncer: Some(3), nominator: Some(2) }] + ); + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(1), nominator: Some(2), bouncer: Some(3) }, + ); + + // Noop works + assert_ok!(Pools::update_roles( + RuntimeOrigin::root(), + 1, + ConfigOp::Set(11), + ConfigOp::Noop, + ConfigOp::Noop + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { root: Some(11), bouncer: Some(3), nominator: Some(2) }] + ); + + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(11), nominator: Some(2), bouncer: Some(3) }, + ); + + // Remove works + assert_ok!(Pools::update_roles( + RuntimeOrigin::root(), + 1, + ConfigOp::Set(69), + ConfigOp::Remove, + ConfigOp::Remove + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { root: Some(69), bouncer: None, nominator: None }] + ); + + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(69), nominator: None, bouncer: None }, + ); + }) + } +} + +mod reward_counter_precision { + use super::*; + + const DOT: Balance = 10u128.pow(10u32); + const POLKADOT_TOTAL_ISSUANCE_GENESIS: Balance = DOT * 10u128.pow(9u32); + + const fn inflation(years: u128) -> u128 { + let mut i = 0; + let mut start = POLKADOT_TOTAL_ISSUANCE_GENESIS; + while i < years { + start = start + start / 10; + i += 1 + } + start + } + + fn default_pool_reward_counter() -> FixedU128 { + let bonded_pool = BondedPools::::get(1).unwrap(); + RewardPools::::get(1) + .unwrap() + .current_reward_counter(1, bonded_pool.points, bonded_pool.commission.current()) + .unwrap() + .0 + } + + fn pending_rewards(of: AccountId) -> Option> { + let member = PoolMembers::::get(of).unwrap(); + assert_eq!(member.pool_id, 1); + let rc = default_pool_reward_counter(); + member.pending_rewards(rc).ok() + } + + #[test] + fn smallest_claimable_reward() { + // create a pool that has all of the polkadot issuance in 50 years. + let pool_bond = inflation(50); + ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 1173908528796953165005, + joined: true, + } + ] + ); + + // the smallest reward that this pool can handle is + let expected_smallest_reward = inflation(50) / 10u128.pow(18); + + // tad bit less. cannot be paid out. + deposit_rewards(expected_smallest_reward - 1); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_eq!(pool_events_since_last_call(), vec![]); + // revert it. + + remove_rewards(expected_smallest_reward - 1); + + // tad bit more. can be claimed. + deposit_rewards(expected_smallest_reward + 1); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 1173 }] + ); + }) + } + + #[test] + fn massive_reward_in_small_pool() { + let tiny_bond = 1000 * DOT; + ExtBuilder::default().ed(DOT).min_bond(tiny_bond).build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10000000000000, joined: true } + ] + ); + + Balances::make_free_balance_be(&20, tiny_bond); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), tiny_bond / 2, 1)); + + // Suddenly, add a shit ton of rewards. + deposit_rewards(inflation(1)); + + // now claim. + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Bonded { member: 20, pool_id: 1, bonded: 5000000000000, joined: true }, + Event::PaidOut { member: 10, pool_id: 1, payout: 7333333333333333333 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 3666666666666666666 } + ] + ); + }) + } + + #[test] + fn reward_counter_calc_wont_fail_in_normal_polkadot_future() { + // create a pool that has roughly half of the polkadot issuance in 10 years. + let pool_bond = inflation(10) / 2; + ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 12_968_712_300_500_000_000, + joined: true, + } + ] + ); + + // in 10 years, the total claimed rewards are large values as well. assuming that a pool + // is earning all of the inflation per year (which is really unrealistic, but worse + // case), that will be: + let pool_total_earnings_10_years = inflation(10) - POLKADOT_TOTAL_ISSUANCE_GENESIS; + deposit_rewards(pool_total_earnings_10_years); + + // some whale now joins with the other half ot the total issuance. This will bloat all + // the calculation regarding current reward counter. + Balances::make_free_balance_be(&20, pool_bond * 2); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), pool_bond, 1)); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::Bonded { + member: 20, + pool_id: 1, + bonded: 12_968_712_300_500_000_000, + joined: true + }] + ); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 15937424600999999996 }] + ); + + // now let a small member join with 10 DOTs. + Balances::make_free_balance_be(&30, 20 * DOT); + assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10 * DOT, 1)); + + // and give a reasonably small reward to the pool. + deposit_rewards(DOT); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Bonded { member: 30, pool_id: 1, bonded: 100000000000, joined: true }, + // quite small, but working fine. + Event::PaidOut { member: 30, pool_id: 1, payout: 38 } + ] + ); + }) + } + + #[test] + fn reward_counter_update_can_fail_if_pool_is_highly_slashed() { + // create a pool that has roughly half of the polkadot issuance in 10 years. + let pool_bond = inflation(10) / 2; + ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 12_968_712_300_500_000_000, + joined: true, + } + ] + ); + + // slash this pool by 99% of that. + StakingMock::set_bonded_balance(default_bonded_account(), DOT + pool_bond / 100); + + // some whale now joins with the other half ot the total issuance. This will trigger an + // overflow. This test is actually a bit too lenient because all the reward counters are + // set to zero. In other tests that we want to assert a scenario won't fail, we should + // also set the reward counters to some large value. + Balances::make_free_balance_be(&20, pool_bond * 2); + assert_err!( + Pools::join(RuntimeOrigin::signed(20), pool_bond, 1), + Error::::OverflowRisk + ); + }) + } + + #[test] + fn if_small_member_waits_long_enough_they_will_earn_rewards() { + // create a pool that has a quarter of the current polkadot issuance + ExtBuilder::default() + .ed(DOT) + .min_bond(POLKADOT_TOTAL_ISSUANCE_GENESIS / 4) + .build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 2500000000000000000, + joined: true, + } + ] + ); + + // and have a tiny fish join the pool as well.. + Balances::make_free_balance_be(&20, 20 * DOT); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10 * DOT, 1)); + + // earn some small rewards + deposit_rewards(DOT / 1000); + + // no point in claiming for 20 (nonetheless, it should be harmless) + assert!(pending_rewards(20).unwrap().is_zero()); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Bonded { + member: 20, + pool_id: 1, + bonded: 100000000000, + joined: true + }, + Event::PaidOut { member: 10, pool_id: 1, payout: 9999997 } + ] + ); + + // earn some small more, still nothing can be claimed for 20, but 10 claims their + // share. + deposit_rewards(DOT / 1000); + assert!(pending_rewards(20).unwrap().is_zero()); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10000000 }] + ); + + // earn some more rewards, this time 20 can also claim. + deposit_rewards(DOT / 1000); + assert_eq!(pending_rewards(20).unwrap(), 1); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 10000000 }, + Event::PaidOut { member: 20, pool_id: 1, payout: 1 } + ] + ); + }); + } + + #[test] + fn zero_reward_claim_does_not_update_reward_counter() { + // create a pool that has a quarter of the current polkadot issuance + ExtBuilder::default() + .ed(DOT) + .min_bond(POLKADOT_TOTAL_ISSUANCE_GENESIS / 4) + .build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 2500000000000000000, + joined: true, + } + ] + ); + + // and have a tiny fish join the pool as well.. + Balances::make_free_balance_be(&20, 20 * DOT); + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10 * DOT, 1)); + + // earn some small rewards + deposit_rewards(DOT / 1000); + + // if 20 claims now, their reward counter should stay the same, so that they have a + // chance of claiming this if they let it accumulate. Also see + // `if_small_member_waits_long_enough_they_will_earn_rewards` + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Bonded { + member: 20, + pool_id: 1, + bonded: 100000000000, + joined: true + }, + Event::PaidOut { member: 10, pool_id: 1, payout: 9999997 } + ] + ); + + let current_reward_counter = default_pool_reward_counter(); + // has been updated, because they actually claimed something. + assert_eq!( + PoolMembers::::get(10).unwrap().last_recorded_reward_counter, + current_reward_counter + ); + // has not be updated, even though the claim transaction went through okay. + assert_eq!( + PoolMembers::::get(20).unwrap().last_recorded_reward_counter, + Default::default() + ); + }); + } +} + +mod commission { + use super::*; + + #[test] + fn set_commission_works() { + ExtBuilder::default().build_and_execute(|| { + let pool_id = 1; + let root = 900; + + // Commission can be set by the `root` role. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + pool_id, + Some((Perbill::from_percent(50), root)) + )); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id }, + Event::Bonded { member: 10, pool_id, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(50), root)) + }, + ] + ); + + // Commission can be updated only, while keeping the same payee. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + 1, + Some((Perbill::from_percent(25), root)) + )); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(25), root)) + },] + ); + + // Payee can be updated only, while keeping the same commission. + + // Given: + let payee = 901; + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + pool_id, + Some((Perbill::from_percent(25), payee)) + )); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(25), payee)) + },] + ); + + // Pool earns 80 points and a payout is triggered. + + // Given: + deposit_rewards(80); + assert_eq!( + PoolMembers::::get(10).unwrap(), + PoolMember:: { pool_id, points: 10, ..Default::default() } + ); + + // When: + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id, payout: 60 }] + ); + assert_eq!(RewardPool::::current_balance(pool_id), 20); + + // Pending pool commission can be claimed by the root role. + + // When: + assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(root), pool_id)); + + // Then: + assert_eq!(RewardPool::::current_balance(pool_id), 0); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionClaimed { pool_id: 1, commission: 20 }] + ); + + // Commission can be removed from the pool completely. + + // When: + assert_ok!(Pools::set_commission(RuntimeOrigin::signed(root), pool_id, None)); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { pool_id, current: None },] + ); + + // Given a pool now has a reward counter history, additional rewards and payouts can be + // made while maintaining a correct ledger of the reward pool. Pool earns 100 points, + // payout is triggered. + // + // Note that the `total_commission_pending` will not be updated until `update_records` + // is next called, which is not done in this test segment.. + + // Given: + deposit_rewards(100); + + // When: + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id, payout: 100 },] + ); + assert_eq!( + RewardPools::::get(pool_id).unwrap(), + RewardPool { + last_recorded_reward_counter: FixedU128::from_float(6.0), + last_recorded_total_payouts: 80, + total_rewards_claimed: 160, + total_commission_pending: 0, + total_commission_claimed: 20 + } + ); + + // When set commission is called again, update_records is called and + // `total_commission_pending` is updated, based on the current reward counter and pool + // balance. + // + // Note that commission is now 0%, so it should not come into play with subsequent + // payouts. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + 1, + Some((Perbill::from_percent(10), root)) + )); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(10), root)) + },] + ); + assert_eq!( + RewardPools::::get(pool_id).unwrap(), + RewardPool { + last_recorded_reward_counter: FixedU128::from_float(16.0), + last_recorded_total_payouts: 180, + total_rewards_claimed: 160, + total_commission_pending: 0, + total_commission_claimed: 20 + } + ); + + // Supplying a 0% commission along with a payee results in a `None` current value. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + pool_id, + Some((Perbill::from_percent(0), root)) + )); + + // Then: + assert_eq!( + BondedPool::::get(1).unwrap().commission, + Commission { current: None, max: None, change_rate: None, throttle_from: Some(1) } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(0), root)) + },] + ); + + // The payee can be updated even when commission has reached maximum commission. Both + // commission and max commission are set to 10% to test this. + + // Given: + assert_ok!(Pools::set_commission_max( + RuntimeOrigin::signed(root), + pool_id, + Perbill::from_percent(10) + )); + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + pool_id, + Some((Perbill::from_percent(10), root)) + )); + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + pool_id, + Some((Perbill::from_percent(10), payee)) + )); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolMaxCommissionUpdated { + pool_id, + max_commission: Perbill::from_percent(10) + }, + Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(10), root)) + }, + Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(10), payee)) + } + ] + ); + }); + } + + #[test] + fn commission_reward_counter_works_one_member() { + ExtBuilder::default().build_and_execute(|| { + let pool_id = 1; + let root = 900; + let member = 10; + + // Set the pool commission to 10% to test commission shares. Pool is topped up 40 points + // and `member` immediately claims their pending rewards. Reward pooll should still have + // 10% share. + + // Given: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + 1, + Some((Perbill::from_percent(10), root)), + )); + deposit_rewards(40); + + // When: + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!(RewardPool::::current_balance(pool_id), 4); + + // Set pool commission to 20% and repeat the same process. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(root), + 1, + Some((Perbill::from_percent(20), root)), + )); + + // Then: + assert_eq!( + RewardPools::::get(pool_id).unwrap(), + RewardPool { + last_recorded_reward_counter: FixedU128::from_float(3.6), + last_recorded_total_payouts: 40, + total_rewards_claimed: 36, + total_commission_pending: 4, + total_commission_claimed: 0 + } + ); + + // The current reward counter should yield the correct pending rewards of zero. + + // Given: + let (current_reward_counter, _) = RewardPools::::get(pool_id) + .unwrap() + .current_reward_counter( + pool_id, + BondedPools::::get(pool_id).unwrap().points, + Perbill::from_percent(20), + ) + .unwrap(); + + // Then: + assert_eq!( + PoolMembers::::get(member) + .unwrap() + .pending_rewards(current_reward_counter) + .unwrap(), + 0 + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(10), 900)) + }, + Event::PaidOut { member: 10, pool_id: 1, payout: 36 }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(20), 900)) + } + ] + ); + }) + } + + #[test] + fn set_commission_handles_errors() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + ] + ); + + // Provided pool does not exist. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 9999, + Some((Perbill::from_percent(1), 900)), + ), + Error::::PoolNotFound + ); + + // Sender does not have permission to set commission. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(1), + 1, + Some((Perbill::from_percent(5), 900)), + ), + Error::::DoesNotHavePermission + ); + + // Commission increases will be throttled if outside of change_rate allowance. + // Commission is set to 5%. + // Change rate is set to 1% max increase, 2 block delay. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(5), 900)), + )); + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 2_u64 } + )); + assert_eq!( + BondedPool::::get(1).unwrap().commission, + Commission { + current: Some((Perbill::from_percent(5), 900)), + max: None, + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 2_u64 + }), + throttle_from: Some(1_u64), + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(5), 900)) + }, + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 2 + } + } + ] + ); + + // Now try to increase commission to 10% (5% increase). This should be throttled. + // Then: + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(10), 900)) + ), + Error::::CommissionChangeThrottled + ); + + run_blocks(2); + + // Increase commission by 1% and provide an initial payee. This should succeed and set + // the `throttle_from` field. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(6), 900)) + )); + assert_eq!( + BondedPool::::get(1).unwrap().commission, + Commission { + current: Some((Perbill::from_percent(6), 900)), + max: None, + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 2_u64 + }), + throttle_from: Some(3_u64), + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(6), 900)) + },] + ); + + // Attempt to increase the commission an additional 1% (now 7%). This will fail as + // `throttle_from` is now the current block. At least 2 blocks need to pass before we + // can set commission again. + + // Then: + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(7), 900)) + ), + Error::::CommissionChangeThrottled + ); + + run_blocks(2); + + // Can now successfully increase the commission again, to 7%. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(7), 900)), + )); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(7), 900)) + },] + ); + + run_blocks(2); + + // Now surpassed the `min_delay` threshold, but the `max_increase` threshold is + // still at play. An attempted commission change now to 8% (+2% increase) should fail. + + // Then: + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(9), 900)), + ), + Error::::CommissionChangeThrottled + ); + + // Now set a max commission to the current 5%. This will also update the current + // commission to 5%. + + // When: + assert_ok!(Pools::set_commission_max( + RuntimeOrigin::signed(900), + 1, + Perbill::from_percent(5) + )); + assert_eq!( + BondedPool::::get(1).unwrap().commission, + Commission { + current: Some((Perbill::from_percent(5), 900)), + max: Some(Perbill::from_percent(5)), + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 2 + }), + throttle_from: Some(7) + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(5), 900)) + }, + Event::PoolMaxCommissionUpdated { + pool_id: 1, + max_commission: Perbill::from_percent(5) + } + ] + ); + + // Run 2 blocks into the future so we are eligible to update commission again. + run_blocks(2); + + // Now attempt again to increase the commission by 1%, to 6%. This is within the change + // rate allowance, but `max_commission` will now prevent us from going any higher. + + // Then: + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(6), 900)), + ), + Error::::CommissionExceedsMaximum + ); + }); + } + + #[test] + fn set_commission_max_works_with_error_tests() { + ExtBuilder::default().build_and_execute(|| { + // Provided pool does not exist + assert_noop!( + Pools::set_commission_max( + RuntimeOrigin::signed(900), + 9999, + Perbill::from_percent(1) + ), + Error::::PoolNotFound + ); + // Sender does not have permission to set commission + assert_noop!( + Pools::set_commission_max(RuntimeOrigin::signed(1), 1, Perbill::from_percent(5)), + Error::::DoesNotHavePermission + ); + + // Cannot set max commission above GlobalMaxCommission + assert_noop!( + Pools::set_commission_max( + RuntimeOrigin::signed(900), + 1, + Perbill::from_percent(100) + ), + Error::::CommissionExceedsGlobalMaximum + ); + + // Set a max commission commission pool 1 to 80% + assert_ok!(Pools::set_commission_max( + RuntimeOrigin::signed(900), + 1, + Perbill::from_percent(80) + )); + assert_eq!( + BondedPools::::get(1).unwrap().commission.max, + Some(Perbill::from_percent(80)) + ); + + // We attempt to increase the max commission to 90%, but increasing is + // disallowed due to pool's max commission. + assert_noop!( + Pools::set_commission_max(RuntimeOrigin::signed(900), 1, Perbill::from_percent(90)), + Error::::MaxCommissionRestricted + ); + + // We will now set a commission to 75% and then amend the max commission + // to 50%. The max commission change should decrease the current + // commission to 50%. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(75), 900)) + )); + assert_ok!(Pools::set_commission_max( + RuntimeOrigin::signed(900), + 1, + Perbill::from_percent(50) + )); + assert_eq!( + BondedPools::::get(1).unwrap().commission, + Commission { + current: Some((Perbill::from_percent(50), 900)), + max: Some(Perbill::from_percent(50)), + change_rate: None, + throttle_from: Some(1), + } + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolMaxCommissionUpdated { + pool_id: 1, + max_commission: Perbill::from_percent(80) + }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(75), 900)) + }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(50), 900)) + }, + Event::PoolMaxCommissionUpdated { + pool_id: 1, + max_commission: Perbill::from_percent(50) + } + ] + ); + }); + } + + #[test] + fn set_commission_change_rate_works_with_errors() { + ExtBuilder::default().build_and_execute(|| { + // Provided pool does not exist + assert_noop!( + Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 9999, + CommissionChangeRate { + max_increase: Perbill::from_percent(5), + min_delay: 1000_u64 + } + ), + Error::::PoolNotFound + ); + // Sender does not have permission to set commission + assert_noop!( + Pools::set_commission_change_rate( + RuntimeOrigin::signed(1), + 1, + CommissionChangeRate { + max_increase: Perbill::from_percent(5), + min_delay: 1000_u64 + } + ), + Error::::DoesNotHavePermission + ); + + // Set a commission change rate for pool 1 + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 10_u64 } + )); + assert_eq!( + BondedPools::::get(1).unwrap().commission.change_rate, + Some(CommissionChangeRate { + max_increase: Perbill::from_percent(5), + min_delay: 10_u64 + }) + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(5), + min_delay: 10 + } + }, + ] + ); + + // We now try to half the min_delay - this will be disallowed. A greater delay between + // commission changes is seen as more restrictive. + assert_noop!( + Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { + max_increase: Perbill::from_percent(5), + min_delay: 5_u64 + } + ), + Error::::CommissionChangeRateNotAllowed + ); + + // We now try to increase the allowed max_increase - this will fail. A smaller allowed + // commission change is seen as more restrictive. + assert_noop!( + Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { + max_increase: Perbill::from_percent(10), + min_delay: 10_u64 + } + ), + Error::::CommissionChangeRateNotAllowed + ); + + // Successful more restrictive change of min_delay with the current max_increase + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 20_u64 } + )); + + // Successful more restrictive change of max_increase with the current min_delay + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(4), min_delay: 20_u64 } + )); + + // Successful more restrictive change of both max_increase and min_delay + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(3), min_delay: 30_u64 } + )); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(5), + min_delay: 20 + } + }, + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(4), + min_delay: 20 + } + }, + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(3), + min_delay: 30 + } + } + ] + ); + }); + } + + #[test] + fn change_rate_does_not_apply_to_decreasing_commission() { + ExtBuilder::default().build_and_execute(|| { + // set initial commission of the pool to 10%. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(10), 900)) + )); + + // Set a commission change rate for pool 1, 1% every 10 blocks + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 10_u64 } + )); + assert_eq!( + BondedPools::::get(1).unwrap().commission.change_rate, + Some(CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 10_u64 + }) + ); + + // run `min_delay` blocks to allow a commission update. + run_blocks(10_u64); + + // Test `max_increase`: attempt to decrease the commission by 5%. Should succeed. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(5), 900)) + )); + + // Test `min_delay`: *immediately* attempt to decrease the commission by 2%. Should + // succeed. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(3), 900)) + )); + + // Attempt to *increase* the commission by 5%. Should fail. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(8), 900)) + ), + Error::::CommissionChangeThrottled + ); + + // Sanity check: the resulting pool Commission state. + assert_eq!( + BondedPools::::get(1).unwrap().commission, + Commission { + current: Some((Perbill::from_percent(3), 900)), + max: None, + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 10_u64 + }), + throttle_from: Some(11), + } + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(10), 900)) + }, + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 10 + } + }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(5), 900)) + }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(3), 900)) + } + ] + ); + }); + } + + #[test] + fn set_commission_max_to_zero_works() { + ExtBuilder::default().build_and_execute(|| { + // 0% max commission test. + // set commission max 0%. + assert_ok!(Pools::set_commission_max(RuntimeOrigin::signed(900), 1, Zero::zero())); + + // a max commission of 0% essentially freezes the current commission, even when None. + // All commission update attempts will fail. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(1), 900)) + ), + Error::::CommissionExceedsMaximum + ); + }) + } + + #[test] + fn set_commission_change_rate_zero_max_increase_works() { + ExtBuilder::default().build_and_execute(|| { + // set commission change rate to 0% per 10 blocks + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(0), min_delay: 10_u64 } + )); + + // even though there is a min delay of 10 blocks, a max increase of 0% essentially + // freezes the commission. All commission update attempts will fail. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(1), 900)) + ), + Error::::CommissionChangeThrottled + ); + }) + } + + #[test] + fn set_commission_change_rate_zero_min_delay_works() { + ExtBuilder::default().build_and_execute(|| { + // set commission change rate to 1% with a 0 block `min_delay`. + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 0_u64 } + )); + assert_eq!( + BondedPools::::get(1).unwrap().commission, + Commission { + current: None, + max: None, + change_rate: Some(CommissionChangeRate { + max_increase: Perbill::from_percent(1), + min_delay: 0 + }), + throttle_from: Some(1) + } + ); + + // since there is no min delay, we should be able to immediately set the commission. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(1), 900)) + )); + + // sanity check: increasing again to more than +1% will fail. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(3), 900)) + ), + Error::::CommissionChangeThrottled + ); + }) + } + + #[test] + fn set_commission_change_rate_zero_value_works() { + ExtBuilder::default().build_and_execute(|| { + // Check zero values play nice. 0 `min_delay` and 0% max_increase test. + // set commission change rate to 0% per 0 blocks. + assert_ok!(Pools::set_commission_change_rate( + RuntimeOrigin::signed(900), + 1, + CommissionChangeRate { max_increase: Perbill::from_percent(0), min_delay: 0_u64 } + )); + + // even though there is no min delay, a max increase of 0% essentially freezes the + // commission. All commission update attempts will fail. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + 1, + Some((Perbill::from_percent(1), 900)) + ), + Error::::CommissionChangeThrottled + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionChangeRateUpdated { + pool_id: 1, + change_rate: CommissionChangeRate { + max_increase: Perbill::from_percent(0), + min_delay: 0_u64 + } + } + ] + ); + }) + } + + #[test] + fn do_reward_payout_with_various_commissions() { + ExtBuilder::default().build_and_execute(|| { + // turn off GlobalMaxCommission for this test. + GlobalMaxCommission::::set(None); + let pool_id = 1; + + // top up commission payee account to existential deposit + let _ = Balances::deposit_creating(&2, 5); + + // Set a commission pool 1 to 33%, with a payee set to `2` + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + pool_id, + Some((Perbill::from_percent(33), 2)), + )); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(33), 2)) + }, + ] + ); + + // The pool earns 10 points + deposit_rewards(10); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 7 },] + ); + + // The pool earns 17 points + deposit_rewards(17); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 11 },] + ); + + // The pool earns 50 points + deposit_rewards(50); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 34 },] + ); + + // The pool earns 10439 points + deposit_rewards(10439); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id: 1, payout: 6994 },] + ); + + // Set the commission to 100% and ensure the following payout to the pool member will + // not happen. + + // When: + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + pool_id, + Some((Perbill::from_percent(100), 2)), + )); + + // Given: + deposit_rewards(200); + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(100), 2)) + },] + ); + }) + } + + #[test] + fn commission_accumulates_on_multiple_rewards() { + ExtBuilder::default().build_and_execute(|| { + let pool_id = 1; + + // Given: + + // Set initial commission of pool 1 to 10%. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + pool_id, + Some((Perbill::from_percent(10), 2)), + )); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(10), 2)) + }, + ] + ); + + // When: + + // The pool earns 100 points + deposit_rewards(100); + + // Change commission to 20% + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + pool_id, + Some((Perbill::from_percent(20), 2)), + )); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(20), 2)) + },] + ); + + // The pool earns 100 points + deposit_rewards(100); + + // Then: + + // Claim payout: + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Claim commission: + assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); + + // Then: + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 90 + 80 }, + Event::PoolCommissionClaimed { pool_id: 1, commission: 30 } + ] + ); + }) + } + + #[test] + fn last_recorded_total_payouts_needs_commission() { + ExtBuilder::default().build_and_execute(|| { + let pool_id = 1; + + // Given: + + // Set initial commission of pool 1 to 10%. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + pool_id, + Some((Perbill::from_percent(10), 2)), + )); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(10), 2)) + }, + ] + ); + + // When: + + // The pool earns 100 points + deposit_rewards(100); + + // Claim payout: + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + + // Claim commission: + assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); + + // Then: + + assert_eq!( + RewardPools::::get(1).unwrap().last_recorded_total_payouts, + 90 + 10 + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 10, pool_id: 1, payout: 90 }, + Event::PoolCommissionClaimed { pool_id: 1, commission: 10 } + ] + ); + }) + } + + #[test] + fn do_reward_payout_with_100_percent_commission() { + ExtBuilder::default().build_and_execute(|| { + // turn off GlobalMaxCommission for this test. + GlobalMaxCommission::::set(None); + + let (mut member, bonded_pool, mut reward_pool) = + Pools::get_member_with_pools(&10).unwrap(); + + // top up commission payee account to existential deposit + let _ = Balances::deposit_creating(&2, 5); + + // Set a commission pool 1 to 100%, with a payee set to `2` + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + bonded_pool.id, + Some((Perbill::from_percent(100), 2)), + )); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(100), 2)) + } + ] + ); + + // The pool earns 10 points + deposit_rewards(10); + + // execute the payout + assert_ok!(Pools::do_reward_payout( + &10, + &mut member, + &mut BondedPool::::get(1).unwrap(), + &mut reward_pool + )); + }) + } + + #[test] + fn global_max_caps_max_commission_payout() { + ExtBuilder::default().build_and_execute(|| { + // Note: GlobalMaxCommission is set at 90%. + + let (mut member, bonded_pool, mut reward_pool) = + Pools::get_member_with_pools(&10).unwrap(); + + // top up the commission payee account to existential deposit + let _ = Balances::deposit_creating(&2, 5); + + // Set a commission pool 1 to 100% fails. + assert_noop!( + Pools::set_commission( + RuntimeOrigin::signed(900), + bonded_pool.id, + Some((Perbill::from_percent(100), 2)), + ), + Error::::CommissionExceedsGlobalMaximum + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + ] + ); + + // Set pool commission to 90% and then set global max commission to 80%. + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + bonded_pool.id, + Some((Perbill::from_percent(90), 2)), + )); + GlobalMaxCommission::::set(Some(Perbill::from_percent(80))); + + // The pool earns 10 points + deposit_rewards(10); + + // execute the payout + assert_ok!(Pools::do_reward_payout( + &10, + &mut member, + &mut BondedPool::::get(1).unwrap(), + &mut reward_pool + )); + + // Confirm the commission was only 8 points out of 10 points, and the payout was 2 out + // of 10 points, reflecting the 80% global max commission. + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolCommissionUpdated { + pool_id: 1, + current: Some((Perbill::from_percent(90), 2)) + }, + Event::PaidOut { member: 10, pool_id: 1, payout: 2 }, + ] + ); + }) + } + + #[test] + fn claim_commission_works() { + ExtBuilder::default().build_and_execute(|| { + let pool_id = 1; + + let _ = Balances::deposit_creating(&900, 5); + assert_ok!(Pools::set_commission( + RuntimeOrigin::signed(900), + pool_id, + Some((Perbill::from_percent(50), 900)) + )); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id }, + Event::Bonded { member: 10, pool_id, bonded: 10, joined: true }, + Event::PoolCommissionUpdated { + pool_id, + current: Some((Perbill::from_percent(50), 900)) + }, + ] + ); + + // Pool earns 80 points, payout is triggered. + deposit_rewards(80); + assert_eq!( + PoolMembers::::get(10).unwrap(), + PoolMember:: { pool_id, points: 10, ..Default::default() } + ); + + assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); + assert_eq!( + pool_events_since_last_call(), + vec![Event::PaidOut { member: 10, pool_id, payout: 40 }] + ); + + // Given: + assert_eq!(RewardPool::::current_balance(pool_id), 40); + + // Pool does not exist + assert_noop!( + Pools::claim_commission(RuntimeOrigin::signed(900), 9999,), + Error::::PoolNotFound + ); + + // Does not have permission. + assert_noop!( + Pools::claim_commission(RuntimeOrigin::signed(10), pool_id,), + Error::::DoesNotHavePermission + ); + + // When: + assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); + + // Then: + assert_eq!(RewardPool::::current_balance(pool_id), 0); + + // No more pending commission. + assert_noop!( + Pools::claim_commission(RuntimeOrigin::signed(900), pool_id,), + Error::::NoPendingCommission + ); + }) + } +} diff --git a/pallets/nomination-pools/src/weights.rs b/pallets/nomination-pools/src/weights.rs new file mode 100644 index 000000000..eb33c9adb --- /dev/null +++ b/pallets/nomination-pools/src/weights.rs @@ -0,0 +1,1140 @@ +// This file is part of Substrate. + +// Copyright (C) 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_nomination_pools +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-06-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-e8ezs4ez-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_nomination_pools +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/nomination-pools/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_nomination_pools. +pub trait WeightInfo { + fn join() -> Weight; + fn bond_extra_transfer() -> Weight; + fn bond_extra_other() -> Weight; + fn claim_payout() -> Weight; + fn unbond() -> Weight; + fn pool_withdraw_unbonded(s: u32, ) -> Weight; + fn withdraw_unbonded_update(s: u32, ) -> Weight; + fn withdraw_unbonded_kill(s: u32, ) -> Weight; + fn create() -> Weight; + fn nominate(n: u32, ) -> Weight; + fn set_state() -> Weight; + fn set_metadata(n: u32, ) -> Weight; + fn set_configs() -> Weight; + fn update_roles() -> Weight; + fn chill() -> Weight; + fn set_commission() -> Weight; + fn set_commission_max() -> Weight; + fn set_commission_change_rate() -> Weight; + fn set_claim_permission() -> Weight; + fn claim_commission() -> Weight; +} + +/// Weights for pallet_nomination_pools using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: NominationPools MinJoinBond (r:1 w:0) + /// Proof: NominationPools MinJoinBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0) + /// Proof: NominationPools MaxPoolMembersPerPool (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembers (r:1 w:0) + /// Proof: NominationPools MaxPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + fn join() -> Weight { + // Proof Size summary in bytes: + // Measured: `3300` + // Estimated: `8877` + // Minimum execution time: 200_966_000 picoseconds. + Weight::from_parts(208_322_000, 8877) + .saturating_add(T::DbWeight::get().reads(19_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:3 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + fn bond_extra_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `3310` + // Estimated: `8877` + // Minimum execution time: 197_865_000 picoseconds. + Weight::from_parts(203_085_000, 8877) + .saturating_add(T::DbWeight::get().reads(16_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)) + } + /// Storage: NominationPools ClaimPermissions (r:1 w:0) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:3 w:3) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + fn bond_extra_other() -> Weight { + // Proof Size summary in bytes: + // Measured: `3375` + // Estimated: `8877` + // Minimum execution time: 235_496_000 picoseconds. + Weight::from_parts(242_104_000, 8877) + .saturating_add(T::DbWeight::get().reads(17_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) + } + /// Storage: NominationPools ClaimPermissions (r:1 w:0) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn claim_payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `1171` + // Estimated: `3702` + // Minimum execution time: 81_813_000 picoseconds. + Weight::from_parts(83_277_000, 3702) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:0) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: Staking MinNominatorBond (r:1 w:0) + /// Proof: Staking MinNominatorBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + /// Storage: NominationPools SubPoolsStorage (r:1 w:1) + /// Proof: NominationPools SubPoolsStorage (max_values: None, max_size: Some(24382), added: 26857, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForSubPoolsStorage (r:1 w:1) + /// Proof: NominationPools CounterForSubPoolsStorage (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn unbond() -> Weight { + // Proof Size summary in bytes: + // Measured: `3586` + // Estimated: `27847` + // Minimum execution time: 183_935_000 picoseconds. + Weight::from_parts(186_920_000, 27847) + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `s` is `[0, 100]`. + fn pool_withdraw_unbonded(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1687` + // Estimated: `4764` + // Minimum execution time: 64_962_000 picoseconds. + Weight::from_parts(67_936_216, 4764) + // Standard Error: 1_780 + .saturating_add(Weight::from_parts(36_110, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools SubPoolsStorage (r:1 w:1) + /// Proof: NominationPools SubPoolsStorage (max_values: None, max_size: Some(24382), added: 26857, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools ClaimPermissions (r:0 w:1) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_update(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2115` + // Estimated: `27847` + // Minimum execution time: 136_073_000 picoseconds. + Weight::from_parts(141_448_439, 27847) + // Standard Error: 2_472 + .saturating_add(Weight::from_parts(75_893, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools SubPoolsStorage (r:1 w:1) + /// Proof: NominationPools SubPoolsStorage (max_values: None, max_size: Some(24382), added: 26857, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:1) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking SlashingSpans (r:1 w:0) + /// Proof Skipped: Staking SlashingSpans (max_values: None, max_size: None, mode: Measured) + /// Storage: Staking Validators (r:1 w:0) + /// Proof: Staking Validators (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:0) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools ReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools ReversePoolIdLookup (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools CounterForReversePoolIdLookup (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForRewardPools (r:1 w:1) + /// Proof: NominationPools CounterForRewardPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForSubPoolsStorage (r:1 w:1) + /// Proof: NominationPools CounterForSubPoolsStorage (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools Metadata (r:1 w:1) + /// Proof: NominationPools Metadata (max_values: None, max_size: Some(270), added: 2745, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForBondedPools (r:1 w:1) + /// Proof: NominationPools CounterForBondedPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking Payee (r:0 w:1) + /// Proof: Staking Payee (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + /// Storage: NominationPools ClaimPermissions (r:0 w:1) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_kill(_s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2470` + // Estimated: `27847` + // Minimum execution time: 230_871_000 picoseconds. + Weight::from_parts(239_533_976, 27847) + .saturating_add(T::DbWeight::get().reads(21_u64)) + .saturating_add(T::DbWeight::get().writes(18_u64)) + } + /// Storage: NominationPools LastPoolId (r:1 w:1) + /// Proof: NominationPools LastPoolId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking MinNominatorBond (r:1 w:0) + /// Proof: Staking MinNominatorBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MinCreateBond (r:1 w:0) + /// Proof: NominationPools MinCreateBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MinJoinBond (r:1 w:0) + /// Proof: NominationPools MinJoinBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPools (r:1 w:0) + /// Proof: NominationPools MaxPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForBondedPools (r:1 w:1) + /// Proof: NominationPools CounterForBondedPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0) + /// Proof: NominationPools MaxPoolMembersPerPool (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembers (r:1 w:0) + /// Proof: NominationPools MaxPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:1) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForRewardPools (r:1 w:1) + /// Proof: NominationPools CounterForRewardPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools ReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools ReversePoolIdLookup (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools CounterForReversePoolIdLookup (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Payee (r:0 w:1) + /// Proof: Staking Payee (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `1289` + // Estimated: `6196` + // Minimum execution time: 194_272_000 picoseconds. + Weight::from_parts(197_933_000, 6196) + .saturating_add(T::DbWeight::get().reads(22_u64)) + .saturating_add(T::DbWeight::get().writes(15_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:0) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking MinNominatorBond (r:1 w:0) + /// Proof: Staking MinNominatorBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:1) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: Staking MaxNominatorsCount (r:1 w:0) + /// Proof: Staking MaxNominatorsCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking Validators (r:17 w:0) + /// Proof: Staking Validators (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:1 w:1) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:1 w:1) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + /// Storage: VoterList CounterForListNodes (r:1 w:1) + /// Proof: VoterList CounterForListNodes (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking CounterForNominators (r:1 w:1) + /// Proof: Staking CounterForNominators (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// The range of component `n` is `[1, 16]`. + fn nominate(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1849` + // Estimated: `4556 + n * (2520 ±0)` + // Minimum execution time: 70_256_000 picoseconds. + Weight::from_parts(71_045_351, 4556) + // Standard Error: 9_898 + .saturating_add(Weight::from_parts(1_592_597, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(12_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(5_u64)) + .saturating_add(Weight::from_parts(0, 2520).saturating_mul(n.into())) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:0) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + fn set_state() -> Weight { + // Proof Size summary in bytes: + // Measured: `1438` + // Estimated: `4556` + // Minimum execution time: 36_233_000 picoseconds. + Weight::from_parts(37_114_000, 4556) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools Metadata (r:1 w:1) + /// Proof: NominationPools Metadata (max_values: None, max_size: Some(270), added: 2745, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForMetadata (r:1 w:1) + /// Proof: NominationPools CounterForMetadata (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// The range of component `n` is `[1, 256]`. + fn set_metadata(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `531` + // Estimated: `3735` + // Minimum execution time: 14_494_000 picoseconds. + Weight::from_parts(15_445_658, 3735) + // Standard Error: 211 + .saturating_add(Weight::from_parts(1_523, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: NominationPools MinJoinBond (r:0 w:1) + /// Proof: NominationPools MinJoinBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembers (r:0 w:1) + /// Proof: NominationPools MaxPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembersPerPool (r:0 w:1) + /// Proof: NominationPools MaxPoolMembersPerPool (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MinCreateBond (r:0 w:1) + /// Proof: NominationPools MinCreateBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:0 w:1) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPools (r:0 w:1) + /// Proof: NominationPools MaxPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn set_configs() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_776_000 picoseconds. + Weight::from_parts(7_033_000, 0) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + fn update_roles() -> Weight { + // Proof Size summary in bytes: + // Measured: `531` + // Estimated: `3685` + // Minimum execution time: 19_586_000 picoseconds. + Weight::from_parts(20_287_000, 3685) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:0) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking Validators (r:1 w:0) + /// Proof: Staking Validators (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:1) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: Staking CounterForNominators (r:1 w:1) + /// Proof: Staking CounterForNominators (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:1 w:1) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:1 w:1) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + /// Storage: VoterList CounterForListNodes (r:1 w:1) + /// Proof: VoterList CounterForListNodes (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn chill() -> Weight { + // Proof Size summary in bytes: + // Measured: `2012` + // Estimated: `4556` + // Minimum execution time: 68_086_000 picoseconds. + Weight::from_parts(70_784_000, 4556) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn set_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `770` + // Estimated: `3685` + // Minimum execution time: 33_353_000 picoseconds. + Weight::from_parts(34_519_000, 3685) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + fn set_commission_max() -> Weight { + // Proof Size summary in bytes: + // Measured: `571` + // Estimated: `3685` + // Minimum execution time: 19_020_000 picoseconds. + Weight::from_parts(19_630_000, 3685) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + fn set_commission_change_rate() -> Weight { + // Proof Size summary in bytes: + // Measured: `531` + // Estimated: `3685` + // Minimum execution time: 19_693_000 picoseconds. + Weight::from_parts(20_114_000, 3685) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:0) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools ClaimPermissions (r:1 w:1) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + fn set_claim_permission() -> Weight { + // Proof Size summary in bytes: + // Measured: `542` + // Estimated: `3702` + // Minimum execution time: 14_810_000 picoseconds. + Weight::from_parts(15_526_000, 3702) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn claim_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `968` + // Estimated: `3685` + // Minimum execution time: 66_400_000 picoseconds. + Weight::from_parts(67_707_000, 3685) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: NominationPools MinJoinBond (r:1 w:0) + /// Proof: NominationPools MinJoinBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0) + /// Proof: NominationPools MaxPoolMembersPerPool (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembers (r:1 w:0) + /// Proof: NominationPools MaxPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + fn join() -> Weight { + // Proof Size summary in bytes: + // Measured: `3300` + // Estimated: `8877` + // Minimum execution time: 200_966_000 picoseconds. + Weight::from_parts(208_322_000, 8877) + .saturating_add(RocksDbWeight::get().reads(19_u64)) + .saturating_add(RocksDbWeight::get().writes(12_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:3 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + fn bond_extra_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `3310` + // Estimated: `8877` + // Minimum execution time: 197_865_000 picoseconds. + Weight::from_parts(203_085_000, 8877) + .saturating_add(RocksDbWeight::get().reads(16_u64)) + .saturating_add(RocksDbWeight::get().writes(12_u64)) + } + /// Storage: NominationPools ClaimPermissions (r:1 w:0) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:3 w:3) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + fn bond_extra_other() -> Weight { + // Proof Size summary in bytes: + // Measured: `3375` + // Estimated: `8877` + // Minimum execution time: 235_496_000 picoseconds. + Weight::from_parts(242_104_000, 8877) + .saturating_add(RocksDbWeight::get().reads(17_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) + } + /// Storage: NominationPools ClaimPermissions (r:1 w:0) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn claim_payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `1171` + // Estimated: `3702` + // Minimum execution time: 81_813_000 picoseconds. + Weight::from_parts(83_277_000, 3702) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:0) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: Staking MinNominatorBond (r:1 w:0) + /// Proof: Staking MinNominatorBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:3 w:3) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:2 w:2) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + /// Storage: NominationPools SubPoolsStorage (r:1 w:1) + /// Proof: NominationPools SubPoolsStorage (max_values: None, max_size: Some(24382), added: 26857, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForSubPoolsStorage (r:1 w:1) + /// Proof: NominationPools CounterForSubPoolsStorage (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn unbond() -> Weight { + // Proof Size summary in bytes: + // Measured: `3586` + // Estimated: `27847` + // Minimum execution time: 183_935_000 picoseconds. + Weight::from_parts(186_920_000, 27847) + .saturating_add(RocksDbWeight::get().reads(20_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `s` is `[0, 100]`. + fn pool_withdraw_unbonded(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1687` + // Estimated: `4764` + // Minimum execution time: 64_962_000 picoseconds. + Weight::from_parts(67_936_216, 4764) + // Standard Error: 1_780 + .saturating_add(Weight::from_parts(36_110, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools SubPoolsStorage (r:1 w:1) + /// Proof: NominationPools SubPoolsStorage (max_values: None, max_size: Some(24382), added: 26857, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools ClaimPermissions (r:0 w:1) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_update(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2115` + // Estimated: `27847` + // Minimum execution time: 136_073_000 picoseconds. + Weight::from_parts(141_448_439, 27847) + // Standard Error: 2_472 + .saturating_add(Weight::from_parts(75_893, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools SubPoolsStorage (r:1 w:1) + /// Proof: NominationPools SubPoolsStorage (max_values: None, max_size: Some(24382), added: 26857, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:1) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking SlashingSpans (r:1 w:0) + /// Proof Skipped: Staking SlashingSpans (max_values: None, max_size: None, mode: Measured) + /// Storage: Staking Validators (r:1 w:0) + /// Proof: Staking Validators (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:0) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools ReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools ReversePoolIdLookup (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools CounterForReversePoolIdLookup (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForRewardPools (r:1 w:1) + /// Proof: NominationPools CounterForRewardPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForSubPoolsStorage (r:1 w:1) + /// Proof: NominationPools CounterForSubPoolsStorage (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools Metadata (r:1 w:1) + /// Proof: NominationPools Metadata (max_values: None, max_size: Some(270), added: 2745, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForBondedPools (r:1 w:1) + /// Proof: NominationPools CounterForBondedPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking Payee (r:0 w:1) + /// Proof: Staking Payee (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + /// Storage: NominationPools ClaimPermissions (r:0 w:1) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_kill(_s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2470` + // Estimated: `27847` + // Minimum execution time: 230_871_000 picoseconds. + Weight::from_parts(239_533_976, 27847) + .saturating_add(RocksDbWeight::get().reads(21_u64)) + .saturating_add(RocksDbWeight::get().writes(18_u64)) + } + /// Storage: NominationPools LastPoolId (r:1 w:1) + /// Proof: NominationPools LastPoolId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking MinNominatorBond (r:1 w:0) + /// Proof: Staking MinNominatorBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MinCreateBond (r:1 w:0) + /// Proof: NominationPools MinCreateBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MinJoinBond (r:1 w:0) + /// Proof: NominationPools MinJoinBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPools (r:1 w:0) + /// Proof: NominationPools MaxPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForBondedPools (r:1 w:1) + /// Proof: NominationPools CounterForBondedPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools PoolMembers (r:1 w:1) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembersPerPool (r:1 w:0) + /// Proof: NominationPools MaxPoolMembersPerPool (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembers (r:1 w:0) + /// Proof: NominationPools MaxPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForPoolMembers (r:1 w:1) + /// Proof: NominationPools CounterForPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:1) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:1) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForRewardPools (r:1 w:1) + /// Proof: NominationPools CounterForRewardPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools ReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools ReversePoolIdLookup (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForReversePoolIdLookup (r:1 w:1) + /// Proof: NominationPools CounterForReversePoolIdLookup (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Payee (r:0 w:1) + /// Proof: Staking Payee (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `1289` + // Estimated: `6196` + // Minimum execution time: 194_272_000 picoseconds. + Weight::from_parts(197_933_000, 6196) + .saturating_add(RocksDbWeight::get().reads(22_u64)) + .saturating_add(RocksDbWeight::get().writes(15_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:0) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking MinNominatorBond (r:1 w:0) + /// Proof: Staking MinNominatorBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:1) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: Staking MaxNominatorsCount (r:1 w:0) + /// Proof: Staking MaxNominatorsCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking Validators (r:17 w:0) + /// Proof: Staking Validators (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + /// Storage: Staking CurrentEra (r:1 w:0) + /// Proof: Staking CurrentEra (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:1 w:1) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:1 w:1) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + /// Storage: VoterList CounterForListNodes (r:1 w:1) + /// Proof: VoterList CounterForListNodes (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Staking CounterForNominators (r:1 w:1) + /// Proof: Staking CounterForNominators (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// The range of component `n` is `[1, 16]`. + fn nominate(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1849` + // Estimated: `4556 + n * (2520 ±0)` + // Minimum execution time: 70_256_000 picoseconds. + Weight::from_parts(71_045_351, 4556) + // Standard Error: 9_898 + .saturating_add(Weight::from_parts(1_592_597, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(12_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + .saturating_add(Weight::from_parts(0, 2520).saturating_mul(n.into())) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:0) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + fn set_state() -> Weight { + // Proof Size summary in bytes: + // Measured: `1438` + // Estimated: `4556` + // Minimum execution time: 36_233_000 picoseconds. + Weight::from_parts(37_114_000, 4556) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools Metadata (r:1 w:1) + /// Proof: NominationPools Metadata (max_values: None, max_size: Some(270), added: 2745, mode: MaxEncodedLen) + /// Storage: NominationPools CounterForMetadata (r:1 w:1) + /// Proof: NominationPools CounterForMetadata (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// The range of component `n` is `[1, 256]`. + fn set_metadata(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `531` + // Estimated: `3735` + // Minimum execution time: 14_494_000 picoseconds. + Weight::from_parts(15_445_658, 3735) + // Standard Error: 211 + .saturating_add(Weight::from_parts(1_523, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: NominationPools MinJoinBond (r:0 w:1) + /// Proof: NominationPools MinJoinBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembers (r:0 w:1) + /// Proof: NominationPools MaxPoolMembers (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPoolMembersPerPool (r:0 w:1) + /// Proof: NominationPools MaxPoolMembersPerPool (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MinCreateBond (r:0 w:1) + /// Proof: NominationPools MinCreateBond (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:0 w:1) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: NominationPools MaxPools (r:0 w:1) + /// Proof: NominationPools MaxPools (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn set_configs() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_776_000 picoseconds. + Weight::from_parts(7_033_000, 0) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + fn update_roles() -> Weight { + // Proof Size summary in bytes: + // Measured: `531` + // Estimated: `3685` + // Minimum execution time: 19_586_000 picoseconds. + Weight::from_parts(20_287_000, 3685) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: Staking Bonded (r:1 w:0) + /// Proof: Staking Bonded (max_values: None, max_size: Some(72), added: 2547, mode: MaxEncodedLen) + /// Storage: Staking Ledger (r:1 w:0) + /// Proof: Staking Ledger (max_values: None, max_size: Some(1091), added: 3566, mode: MaxEncodedLen) + /// Storage: Staking Validators (r:1 w:0) + /// Proof: Staking Validators (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + /// Storage: Staking Nominators (r:1 w:1) + /// Proof: Staking Nominators (max_values: None, max_size: Some(558), added: 3033, mode: MaxEncodedLen) + /// Storage: Staking CounterForNominators (r:1 w:1) + /// Proof: Staking CounterForNominators (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: VoterList ListNodes (r:1 w:1) + /// Proof: VoterList ListNodes (max_values: None, max_size: Some(154), added: 2629, mode: MaxEncodedLen) + /// Storage: VoterList ListBags (r:1 w:1) + /// Proof: VoterList ListBags (max_values: None, max_size: Some(82), added: 2557, mode: MaxEncodedLen) + /// Storage: VoterList CounterForListNodes (r:1 w:1) + /// Proof: VoterList CounterForListNodes (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn chill() -> Weight { + // Proof Size summary in bytes: + // Measured: `2012` + // Estimated: `4556` + // Minimum execution time: 68_086_000 picoseconds. + Weight::from_parts(70_784_000, 4556) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn set_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `770` + // Estimated: `3685` + // Minimum execution time: 33_353_000 picoseconds. + Weight::from_parts(34_519_000, 3685) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + fn set_commission_max() -> Weight { + // Proof Size summary in bytes: + // Measured: `571` + // Estimated: `3685` + // Minimum execution time: 19_020_000 picoseconds. + Weight::from_parts(19_630_000, 3685) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:1) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + fn set_commission_change_rate() -> Weight { + // Proof Size summary in bytes: + // Measured: `531` + // Estimated: `3685` + // Minimum execution time: 19_693_000 picoseconds. + Weight::from_parts(20_114_000, 3685) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools PoolMembers (r:1 w:0) + /// Proof: NominationPools PoolMembers (max_values: None, max_size: Some(237), added: 2712, mode: MaxEncodedLen) + /// Storage: NominationPools ClaimPermissions (r:1 w:1) + /// Proof: NominationPools ClaimPermissions (max_values: None, max_size: Some(41), added: 2516, mode: MaxEncodedLen) + fn set_claim_permission() -> Weight { + // Proof Size summary in bytes: + // Measured: `542` + // Estimated: `3702` + // Minimum execution time: 14_810_000 picoseconds. + Weight::from_parts(15_526_000, 3702) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: NominationPools BondedPools (r:1 w:0) + /// Proof: NominationPools BondedPools (max_values: None, max_size: Some(220), added: 2695, mode: MaxEncodedLen) + /// Storage: NominationPools RewardPools (r:1 w:1) + /// Proof: NominationPools RewardPools (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: NominationPools GlobalMaxCommission (r:1 w:0) + /// Proof: NominationPools GlobalMaxCommission (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn claim_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `968` + // Estimated: `3685` + // Minimum execution time: 66_400_000 picoseconds. + Weight::from_parts(67_707_000, 3685) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } +} diff --git a/pallets/nomination-pools/test-staking/Cargo.toml b/pallets/nomination-pools/test-staking/Cargo.toml new file mode 100644 index 000000000..8c3c43398 --- /dev/null +++ b/pallets/nomination-pools/test-staking/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pallet-nomination-pools-test-staking" +version = "1.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME nomination pools pallet tests with the staking pallet" +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dev-dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1", features = ["derive"] } +scale-info = { version = "2.0.1", features = ["derive"] } + +sp-runtime = { version = "24.0.0" } +sp-io = { version = "23.0.0" } +sp-std = { version = "8.0.0" } +sp-staking = { version = "4.0.0-dev" } +sp-core = { version = "21.0.0" } + +frame-system = { version = "4.0.0-dev" } +frame-support = { version = "4.0.0-dev" } +frame-election-provider-support = { version = "4.0.0-dev" } + +pallet-timestamp = { version = "4.0.0-dev" } +pallet-balances = { version = "4.0.0-dev" } +pallet-staking = { version = "4.0.0-dev" } +pallet-bags-list = { version = "4.0.0-dev" } +pallet-staking-reward-curve = { version = "4.0.0-dev" } +pallet-nomination-pools = { version = "1.0.0-dev", path = ".." } + +sp-tracing = { version = "10.0.0" } +log = { version = "0.4.0" } diff --git a/pallets/nomination-pools/test-staking/src/lib.rs b/pallets/nomination-pools/test-staking/src/lib.rs new file mode 100644 index 000000000..158749258 --- /dev/null +++ b/pallets/nomination-pools/test-staking/src/lib.rs @@ -0,0 +1,693 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +#![cfg(test)] + +mod mock; + +use frame_support::{assert_noop, assert_ok, bounded_btree_map, traits::Currency}; +use mock::*; +use pallet_nomination_pools::{ + BondedPools, Error as PoolsError, Event as PoolsEvent, LastPoolId, PoolMember, PoolMembers, + PoolState, +}; +use pallet_staking::{CurrentEra, Event as StakingEvent, Payee, RewardDestination}; +use sp_runtime::traits::Zero; + +#[test] +fn pool_lifecycle_e2e() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::minimum_balance(), 5); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 50, 10, 10, 10)); + assert_eq!(LastPoolId::::get(), 1); + + // have the pool nominate. + assert_ok!(Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3])); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 50 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 50, joined: true }, + ] + ); + + // have two members join + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(21), 10, 1)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, + PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 10, joined: true }, + ] + ); + + // pool goes into destroying + assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying)); + + // depositor cannot unbond yet. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + PoolsError::::MinimumBondNotMet, + ); + + // now the members want to unbond. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10)); + + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_eras.len(), 1); + assert_eq!(PoolMembers::::get(20).unwrap().points, 0); + assert_eq!(PoolMembers::::get(21).unwrap().unbonding_eras.len(), 1); + assert_eq!(PoolMembers::::get(21).unwrap().points, 0); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + PoolsEvent::Unbonded { member: 20, pool_id: 1, points: 10, balance: 10, era: 3 }, + PoolsEvent::Unbonded { member: 21, pool_id: 1, points: 10, balance: 10, era: 3 }, + ] + ); + + // depositor cannot still unbond + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + PoolsError::::MinimumBondNotMet, + ); + + for e in 1..BondingDuration::get() { + CurrentEra::::set(Some(e)); + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0), + PoolsError::::CannotWithdrawAny + ); + } + + // members are now unlocked. + CurrentEra::::set(Some(BondingDuration::get())); + + // depositor cannot still unbond + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + PoolsError::::MinimumBondNotMet, + ); + + // but members can now withdraw. + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0)); + assert!(PoolMembers::::get(20).is_none()); + assert!(PoolMembers::::get(21).is_none()); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 20 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Withdrawn { member: 20, pool_id: 1, points: 10, balance: 10 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 20 }, + PoolsEvent::Withdrawn { member: 21, pool_id: 1, points: 10, balance: 10 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 21 }, + ] + ); + + // as soon as all members have left, the depositor can try to unbond, but since the + // min-nominator intention is set, they must chill first. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + pallet_staking::Error::::InsufficientBond + ); + + assert_ok!(Pools::chill(RuntimeOrigin::signed(10), 1)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 50)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Chilled { stash: POOL1_BONDED }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 50 }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { member: 10, pool_id: 1, points: 50, balance: 50, era: 6 }] + ); + + // waiting another bonding duration: + CurrentEra::::set(Some(BondingDuration::get() * 2)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 1)); + + // pools is fully destroyed now. + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 50 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Withdrawn { member: 10, pool_id: 1, points: 50, balance: 50 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 10 }, + PoolsEvent::Destroyed { pool_id: 1 } + ] + ); + }) +} + +#[test] +fn pool_slash_e2e() { + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!(LastPoolId::::get(), 1); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + assert_eq!(Payee::::get(POOL1_BONDED), RewardDestination::Account(POOL1_REWARD)); + + // have two members join + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(21), 20, 1)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 20 }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 20 } + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 20, joined: true }, + ] + ); + + // now let's progress a bit. + CurrentEra::::set(Some(1)); + + // 20 / 80 of the total funds are unlocked, and safe from any further slash. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 } + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 }, + PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 4 } + ] + ); + + CurrentEra::::set(Some(2)); + + // note: depositor cannot fully unbond at this point. + // these funds will still get slashed. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + ] + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 5 }, + PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 5 }, + PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: 10, points: 10, era: 5 }, + ] + ); + + // At this point, 20 are safe from slash, 30 are unlocking but vulnerable to slash, and and + // another 30 are active and vulnerable to slash. Let's slash half of them. + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 30, + &mut Default::default(), + &mut Default::default(), + 2, // slash era 2, affects chunks at era 5 onwards. + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 30 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + // 30 has been slashed to 15 (15 slash) + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 5, balance: 15 }, + // 30 has been slashed to 15 (15 slash) + PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 } + ] + ); + + CurrentEra::::set(Some(3)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10)); + + assert_eq!( + PoolMembers::::get(21).unwrap(), + PoolMember { + pool_id: 1, + points: 0, + last_recorded_reward_counter: Zero::zero(), + // the 10 points unlocked just now correspond to 5 points in the unbond pool. + unbonding_eras: bounded_btree_map!(5 => 10, 6 => 5) + } + ); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 5 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: 5, points: 5, era: 6 }] + ); + + // now we start withdrawing. we do it all at once, at era 6 where 20 and 21 are fully free. + CurrentEra::::set(Some(6)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + // 20 had unbonded 10 safely, and 10 got slashed by half. + PoolsEvent::Withdrawn { member: 20, pool_id: 1, balance: 10 + 5, points: 20 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 20 }, + // 21 unbonded all of it after the slash + PoolsEvent::Withdrawn { member: 21, pool_id: 1, balance: 5 + 5, points: 15 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 21 } + ] + ); + assert_eq!( + staking_events_since_last_call(), + // a 10 (un-slashed) + 10/2 (slashed) balance from 10 has also been unlocked + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 15 + 10 + 15 }] + ); + + // now, finally, we can unbond the depositor further than their current limit. + assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 20)); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + PoolsEvent::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 } + ] + ); + + CurrentEra::::set(Some(9)); + assert_eq!( + PoolMembers::::get(10).unwrap(), + PoolMember { + pool_id: 1, + points: 0, + last_recorded_reward_counter: Zero::zero(), + unbonding_eras: bounded_btree_map!(4 => 10, 5 => 10, 9 => 10) + } + ); + // withdraw the depositor, they should lose 12 balance in total due to slash. + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 10 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Withdrawn { member: 10, pool_id: 1, balance: 10 + 15, points: 30 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 10 }, + PoolsEvent::Destroyed { pool_id: 1 } + ] + ); + }); +} + +#[test] +fn pool_slash_proportional() { + // a typical example where 3 pool members unbond in era 99, 100, and 101, and a slash that + // happened in era 100 should only affect the latter two. + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + BondingDuration::set(28); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!(LastPoolId::::get(), 1); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + // have two members join + let bond = 20; + assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(21), bond, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(22), bond, 1)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }, + PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: bond, joined: true }, + PoolsEvent::Bonded { member: 22, pool_id: 1, bonded: bond, joined: true }, + ] + ); + + // now let's progress a lot. + CurrentEra::::set(Some(99)); + + // and unbond + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond)); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 20, + pool_id: 1, + balance: bond, + points: bond, + era: 127 + }] + ); + + CurrentEra::::set(Some(100)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 21, + pool_id: 1, + balance: bond, + points: bond, + era: 128 + }] + ); + + CurrentEra::::set(Some(101)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(22), 22, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 22, + pool_id: 1, + balance: bond, + points: bond, + era: 129 + }] + ); + + // Apply a slash that happened in era 100. This is typically applied with a delay. + // Of the total 100, 50 is slashed. + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 50, + &mut Default::default(), + &mut Default::default(), + 100, + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 50 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + // This era got slashed 12.5, which rounded up to 13. + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 128, balance: 7 }, + // This era got slashed 12 instead of 12.5 because an earlier chunk got 0.5 more + // slashed, and 12 is all the remaining slash + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 129, balance: 8 }, + // Bonded pool got slashed for 25, remaining 15 in it. + PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 } + ] + ); + }); +} + +#[test] +fn pool_slash_non_proportional_only_bonded_pool() { + // A typical example where a pool member unbonds in era 99, and they can get away with a slash + // that happened in era 100, as long as the pool has enough active bond to cover the slash. If + // everything else in the slashing/staking system works, this should always be the case. + // Nonetheless, `ledger.slash` has been written such that it will slash greedily from any chunk + // if it runs out of chunks that it thinks should be affected by the slash. + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + BondingDuration::set(28); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + // have two members join + let bond = 20; + assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }] + ); + + // progress and unbond. + CurrentEra::::set(Some(99)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 20, + pool_id: 1, + balance: bond, + points: bond, + era: 127 + }] + ); + + // slash for 30. This will be deducted only from the bonded pool. + CurrentEra::::set(Some(100)); + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 30, + &mut Default::default(), + &mut Default::default(), + 100, + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 30 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::PoolSlashed { pool_id: 1, balance: 10 }] + ); + }); +} + +#[test] +fn pool_slash_non_proportional_bonded_pool_and_chunks() { + // An uncommon example where even though some funds are unlocked such that they should not be + // affected by a slash, we still slash out of them. This should not happen at all. If a + // nomination has unbonded, from the next era onwards, their exposure will drop, so if an era + // happens in that era, then their share of that slash should naturally be less, such that only + // their active ledger stake is enough to compensate it. + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + BondingDuration::set(28); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + // have two members join + let bond = 20; + assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }] + ); + + // progress and unbond. + CurrentEra::::set(Some(99)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 20, + pool_id: 1, + balance: bond, + points: bond, + era: 127 + }] + ); + + // slash 50. This will be deducted only from the bonded pool and one of the unbonding pools. + CurrentEra::::set(Some(100)); + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 50, + &mut Default::default(), + &mut Default::default(), + 100, + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 50 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + // out of 20, 10 was taken. + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 127, balance: 10 }, + // out of 40, all was taken. + PoolsEvent::PoolSlashed { pool_id: 1, balance: 0 } + ] + ); + }); +} diff --git a/pallets/nomination-pools/test-staking/src/mock.rs b/pallets/nomination-pools/test-staking/src/mock.rs new file mode 100644 index 000000000..ffc1ed56d --- /dev/null +++ b/pallets/nomination-pools/test-staking/src/mock.rs @@ -0,0 +1,269 @@ +// This file is part of Substrate. + +// Copyright (C) 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 frame_election_provider_support::VoteWeight; +use frame_support::{ + assert_ok, + pallet_prelude::*, + parameter_types, + traits::{ConstU64, ConstU8}, + PalletId, +}; +use sp_runtime::{ + traits::{Convert, IdentityLookup}, + BuildStorage, FixedU128, Perbill, +}; + +type AccountId = u128; +type Nonce = u32; +type BlockNumber = u64; +type Balance = u128; + +pub(crate) type T = Runtime; + +pub(crate) const POOL1_BONDED: AccountId = 20318131474730217858575332831085u128; +pub(crate) const POOL1_REWARD: AccountId = 20397359637244482196168876781421u128; + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = Nonce; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + 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 = 5; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type MaxHolds = (); +} + +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; +} + +impl pallet_staking::Config for Runtime { + type MaxNominations = ConstU32<16>; + type Currency = Balances; + type CurrencyBalance = Balance; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type AdminOrigin = frame_system::EnsureRoot; + type BondingDuration = BondingDuration; + type SessionInterface = (); + type EraPayout = pallet_staking::ConvertCurve; + type NextNewSession = (); + type MaxNominatorRewardedPerValidator = ConstU32<64>; + type OffendingValidatorsThreshold = (); + type ElectionProvider = + frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = VoterList; + type TargetList = pallet_staking::UseValidatorsMap; + type MaxUnlockingChunks = ConstU32<32>; + type HistoryDepth = ConstU32<84>; + type EventListeners = Pools; + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type WeightInfo = (); +} + +parameter_types! { + pub static BagThresholds: &'static [VoteWeight] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; +} + +type VoterBagsListInstance = pallet_bags_list::Instance1; +impl pallet_bags_list::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type BagThresholds = BagThresholds; + type ScoreProvider = Staking; + type Score = VoteWeight; +} + +pub struct BalanceToU256; +impl Convert for BalanceToU256 { + fn convert(n: Balance) -> sp_core::U256 { + n.into() + } +} + +pub struct U256ToBalance; +impl Convert 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"); +} + +impl pallet_nomination_pools::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RewardCounter = FixedU128; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = Staking; + type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow; + type MaxMetadataLen = ConstU32<256>; + type MaxUnbonding = ConstU32<8>; + type MaxPointsToBalance = ConstU8<10>; + type PalletId = PoolsPalletId; +} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub struct Runtime + { + System: frame_system::{Pallet, Call, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Staking: pallet_staking::{Pallet, Call, Config, Storage, Event}, + VoterList: pallet_bags_list::::{Pallet, Call, Storage, Event}, + Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event}, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let _ = pallet_nomination_pools::GenesisConfig:: { + min_join_bond: 2, + min_create_bond: 2, + max_pools: Some(3), + max_members_per_pool: Some(5), + max_members: Some(3 * 5), + global_max_commission: Some(Perbill::from_percent(90)), + } + .assimilate_storage(&mut storage) + .unwrap(); + + let _ = pallet_balances::GenesisConfig:: { + balances: vec![(10, 100), (20, 100), (21, 100), (22, 100)], + } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext = sp_io::TestExternalities::from(storage); + + ext.execute_with(|| { + // for events to be deposited. + frame_system::Pallet::::set_block_number(1); + + // set some limit for nominations. + assert_ok!(Staking::set_staking_configs( + RuntimeOrigin::root(), + pallet_staking::ConfigOp::Set(10), // minimum nominator bond + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + )); + }); + + ext +} + +parameter_types! { + static ObservedEventsPools: usize = 0; + static ObservedEventsStaking: usize = 0; + static ObservedEventsBalances: usize = 0; +} + +pub(crate) fn pool_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Pools(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = ObservedEventsPools::get(); + ObservedEventsPools::set(events.len()); + events.into_iter().skip(already_seen).collect() +} + +pub(crate) fn staking_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Staking(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = ObservedEventsStaking::get(); + ObservedEventsStaking::set(events.len()); + events.into_iter().skip(already_seen).collect() +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 66fa66ecc..aecca45a5 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "da-runtime" -version = "7.0.0" +version = "7.0.1" authors = ["Anonymous"] description = "Avail Runtime" edition = "2021" @@ -89,7 +89,8 @@ pallet-mmr = { version = "4.0.0-dev", default-features = false } pallet-multisig = { version = "4.0.0-dev", default-features = false } pallet-child-bounties = { version = "4.0.0-dev", default-features = false } pallet-preimage = { version = "4.0.0-dev", default-features = false } -pallet-nomination-pools = { version = "1", default-features = false } +pallet-nomination-pools = { version = "1.0.0", default-features = false } +pallet-nomination-pools-runtime-api = { version = "1.0.0-dev", default-features = false } pallet-identity = { version = "4.0.0-dev", default-features = false } pallet-mandate = { path = "../pallets/mandate", default-features = false } @@ -181,6 +182,7 @@ std = [ "pallet-membership/std", "pallet-mmr/std", "pallet-multisig/std", + "pallet-nomination-pools-runtime-api/std", "pallet-nomination-pools/std", "pallet-offences/std", "pallet-preimage/std", diff --git a/runtime/src/apis.rs b/runtime/src/apis.rs index f07e536be..249265322 100644 --- a/runtime/src/apis.rs +++ b/runtime/src/apis.rs @@ -24,8 +24,8 @@ use sp_version::RuntimeVersion; use crate::Identity; use crate::{ constants, mmr, AccountId, AuthorityDiscovery, Babe, Block, BlockNumber, EpochDuration, - Executive, Grandpa, Historical, Index, InherentDataExt, Mmr, OpaqueMetadata, Runtime, - RuntimeCall, Seed, SessionKeys, System, TransactionPayment, + Executive, Grandpa, Historical, Index, InherentDataExt, Mmr, NominationPools, OpaqueMetadata, + Runtime, RuntimeCall, Seed, SessionKeys, System, TransactionPayment, }; decl_runtime_apis! { @@ -302,6 +302,24 @@ impl_runtime_apis! { } } + impl pallet_nomination_pools_runtime_api::NominationPoolsApi< + Block, + AccountId, + Balance, + > for Runtime { + fn pending_rewards(member: AccountId) -> Balance { + NominationPools::api_pending_rewards(member).unwrap_or_default() + } + + fn points_to_balance(pool_id: pallet_nomination_pools::PoolId, points: Balance) -> Balance { + NominationPools::api_points_to_balance(pool_id, points) + } + + fn balance_to_points(pool_id: pallet_nomination_pools::PoolId, new_funds: Balance) -> Balance { + NominationPools::api_balance_to_points(pool_id, new_funds) + } + } + impl crate::apis::DataAvailApi for Runtime { fn block_length() -> frame_system::limits::BlockLength { frame_system::Pallet::::block_length() diff --git a/runtime/src/version.rs b/runtime/src/version.rs index c5ec6089f..a878a14c8 100644 --- a/runtime/src/version.rs +++ b/runtime/src/version.rs @@ -17,7 +17,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { /// Per convention: if the runtime behavior changes, increment spec_version /// and set impl_version to 0. This paramenter is typically incremented when /// there's an update to the transaction_version. - spec_version: 14, + spec_version: 15, /// The version of the implementation of the specification. Nodes can ignore this. It is only /// used to indicate that the code is different. As long as the authoring_version and the /// spec_version are the same, the code itself might have changed, but the native and Wasm