// 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, defensive_assert, ensure, pallet_prelude::{MaxEncodedLen, *}, storage::bounded_btree_map::BoundedBTreeMap, traits::{ fungible::{ Inspect as FunInspect, InspectFreeze, Mutate as FunMutate, MutateFreeze as FunMutateFreeze, }, tokens::{Fortitude, Preservation}, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, 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 FunInspect<::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 pool 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 balance of the member, both active and unbonding. /// Doesn't mutate state. #[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))] fn total_balance(&self) -> BalanceOf { let pool = BondedPool::::get(self.pool_id).unwrap(); let active_balance = pool.points_to_balance(self.active_points()); let sub_pools = match SubPoolsStorage::::get(self.pool_id) { Some(sub_pools) => sub_pools, None => return active_balance, }; let unbonding_balance = self.unbonding_eras.iter().fold( BalanceOf::::zero(), |accumulator, (era, unlocked_points)| { // if the `SubPools::with_era` has already been merged into the // `SubPools::no_era` use this pool instead. let era_pool = sub_pools.with_era.get(era).unwrap_or(&sub_pools.no_era); accumulator + (era_pool.point_to_balance(*unlocked_points)) }, ); active_balance + unbonding_balance } /// 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, } // A pool's possible commission claiming permissions. #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum CommissionClaimPermission { Permissionless, Account(AccountId), } /// 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>, // Whether commission can be claimed permissionlessly, or whether an account can claim // commission. `Root` role can always claim. pub claim_permission: 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 transferable provided it is expendable by staking pallet. fn transferable_balance(&self) -> BalanceOf { let account = self.bonded_account(); // Note on why we can't use `Currency::reducible_balance`: Since pooled account has a // provider (staking pallet), the account can not be set expendable by // `pallet-nomination-pool`. This means reducible balance always returns balance preserving // ED in the account. What we want though is transferable balance given the account can be // dusted. T::Currency::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 can_claim_commission(&self, who: &T::AccountId) -> bool { if let Some(permission) = self.commission.claim_permission.as_ref() { match permission { CommissionClaimPermission::Permissionless => true, CommissionClaimPermission::Account(account) => account == who || self.is_root(who), } } else { 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. Increases the [`TotalValueLocked`] /// by `amount`. /// /// If the bond is [`BondType::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 => Preservation::Expendable, BondType::Later => Preservation::Preserve, }, )?; // 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)?, } TotalValueLocked::::mutate(|tvl| { tvl.saturating_accrue(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, }); }; } /// Withdraw all the funds that are already unlocked from staking for the /// [`BondedPool::bonded_account`]. /// /// Also reduces the [`TotalValueLocked`] by the difference of the /// [`T::Staking::total_stake`] of the [`BondedPool::bonded_account`] that might occur by /// [`T::Staking::withdraw_unbonded`]. /// /// Returns the result of [`T::Staking::withdraw_unbonded`] fn withdraw_from_staking(&self, num_slashing_spans: u32) -> Result { let bonded_account = self.bonded_account(); let prev_total = T::Staking::total_stake(&bonded_account.clone()).unwrap_or_default(); let outcome = T::Staking::withdraw_unbonded(bonded_account.clone(), num_slashing_spans); let diff = prev_total .defensive_saturating_sub(T::Staking::total_stake(&bonded_account).unwrap_or_default()); TotalValueLocked::::mutate(|tvl| { tvl.saturating_reduce(diff); }); outcome } } /// 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); // 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. let last_recorded_total_payouts = balance .checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed)) .ok_or(Error::::OverflowRisk)?; // Store the total payouts at the time of this update. // // An increase in ED could cause `last_recorded_total_payouts` to decrease but we should not // allow that to happen since an already paid out reward cannot decrease. The reward account // might go in deficit temporarily in this exceptional case but it will be corrected once // new rewards are added to the pool. self.last_recorded_total_payouts = self.last_recorded_total_payouts.max(last_recorded_total_payouts); 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::reducible_balance( &Pallet::::create_reward_account(id), Preservation::Expendable, Fortitude::Polite, ) } } /// 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(8); #[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 currency type used for nomination pool. type Currency: FunMutate + FunMutateFreeze; /// The overarching freeze reason. type RuntimeFreezeReason: From; /// 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; /// The maximum number of simultaneous unbonding chunks that can exist per member. #[pallet::constant] type MaxUnbonding: 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 sum of funds across all pools. /// /// This might be lower but never higher than the sum of `total_balance` of all [`PoolMembers`] /// because calling `pool_withdraw_unbonded` might decrease the total stake of the pool's /// `bonded_account` without adjusting the pallet-internal `UnbondingPool`'s. #[pallet::storage] pub type TotalValueLocked = StorageValue<_, BalanceOf, ValueQuery>; /// 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 claim permission has been updated. PoolCommissionClaimPermissionUpdated { pool_id: PoolId, permission: Option>, }, /// Pool commission has been claimed. PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf }, /// Topped up deficit in frozen ED of the reward pool. MinBalanceDeficitAdjusted { pool_id: PoolId, amount: BalanceOf }, /// Claimed excess frozen ED of af the reward pool. MinBalanceExcessAdjusted { pool_id: PoolId, amount: 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, /// No imbalance in the ED deposit for the pool. NothingToAdjust, } #[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) } } /// A reason for freezing funds. #[pallet::composite_enum] pub enum FreezeReason { /// Pool reward account is restricted from going below Existential Deposit. #[codec(index = 0)] PoolMinBalance, } #[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 there 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); pool.withdraw_from_staking(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 = bonded_pool.withdraw_from_staking(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.transferable_balance()); T::Currency::transfer( &bonded_pool.bonded_account(), &member_account, balance_to_unbond, Preservation::Expendable, ) .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` transferable. #[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) } /// Top up the deficit or withdraw the excess ED from the pool. /// /// When a pool is created, the pool depositor transfers ED to the reward account of the /// pool. ED is subject to change and over time, the deposit in the reward account may be /// insufficient to cover the ED deficit of the pool or vice-versa where there is excess /// deposit to the pool. This call allows anyone to adjust the ED deposit of the /// pool by either topping up the deficit or claiming the excess. #[pallet::call_index(21)] #[pallet::weight(T::WeightInfo::adjust_pool_deposit())] pub fn adjust_pool_deposit(origin: OriginFor, pool_id: PoolId) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_adjust_pool_deposit(who, pool_id) } /// Set or remove a pool's commission claim permission. /// /// Determines who can claim the pool's pending commission. Only the `Root` role of the pool /// is able to conifigure commission claim permissions. #[pallet::call_index(22)] #[pallet::weight(T::WeightInfo::set_commission_claim_permission())] pub fn set_commission_claim_permission( origin: OriginFor, pool_id: PoolId, permission: Option>, ) -> 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.claim_permission = permission.clone(); bonded_pool.put(); Self::deposit_event(Event::::PoolCommissionClaimPermissionUpdated { pool_id, permission, }); Ok(()) } } #[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); // remove the ED restriction from the pool reward account. let _ = Self::unfreeze_pool_deposit(&bonded_pool.reward_account()).defensive(); // 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. defensive_assert!( frame_system::Pallet::::consumers(&reward_account) == 0, "reward account of dissolving pool should have no consumers" ); defensive_assert!( frame_system::Pallet::::consumers(&bonded_account) == 0, "bonded account of dissolving pool should have no consumers" ); defensive_assert!( T::Staking::total_stake(&bonded_account).unwrap_or_default() == Zero::zero(), "dissolving pool should not have any stake in the staking pallet" ); // This shouldn't fail, but if it does we don't really care. Remaining balance can consist // of unclaimed pending commission, erroneous transfers to the reward account, etc. let reward_pool_remaining = T::Currency::reducible_balance( &reward_account, Preservation::Expendable, Fortitude::Polite, ); let _ = T::Currency::transfer( &reward_account, &bonded_pool.roles.depositor, reward_pool_remaining, Preservation::Expendable, ); defensive_assert!( T::Currency::total_balance(&reward_account) == Zero::zero(), "could not transfer all amount to depositor while dissolving pool" ); defensive_assert!( T::Currency::total_balance(&bonded_pool.bonded_account()) == Zero::zero(), "dissolving pool should not have any balance" ); // NOTE: Defensively force set balance to zero. T::Currency::set_balance(&reward_account, Zero::zero()); T::Currency::set_balance(&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. Preservation::Preserve, )?; 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)?; // Transfer the minimum balance for the reward account. T::Currency::transfer( &who, &bonded_pool.reward_account(), T::Currency::minimum_balance(), Preservation::Expendable, )?; // Restrict reward account balance from going below ED. Self::freeze_pool_deposit(&bonded_pool.reward_account())?; 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_claim_commission(&who), Error::::DoesNotHavePermission); let mut reward_pool = RewardPools::::get(pool_id) .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; // IMPORTANT: ensure 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, Preservation::Preserve, )?; // 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(); 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(()) } fn do_adjust_pool_deposit(who: T::AccountId, pool: PoolId) -> DispatchResult { let bonded_pool = BondedPool::::get(pool).ok_or(Error::::PoolNotFound)?; let reward_acc = &bonded_pool.reward_account(); let pre_frozen_balance = T::Currency::balance_frozen(&FreezeReason::PoolMinBalance.into(), reward_acc); let min_balance = T::Currency::minimum_balance(); if pre_frozen_balance == min_balance { return Err(Error::::NothingToAdjust.into()) } // Update frozen amount with current ED. Self::freeze_pool_deposit(reward_acc)?; if pre_frozen_balance > min_balance { // Transfer excess back to depositor. let excess = pre_frozen_balance.saturating_sub(min_balance); T::Currency::transfer(reward_acc, &who, excess, Preservation::Preserve)?; Self::deposit_event(Event::::MinBalanceExcessAdjusted { pool_id: pool, amount: excess, }); } else { // Transfer ED deficit from depositor to the pool let deficit = min_balance.saturating_sub(pre_frozen_balance); T::Currency::transfer(&who, reward_acc, deficit, Preservation::Expendable)?; Self::deposit_event(Event::::MinBalanceDeficitAdjusted { pool_id: pool, amount: deficit, }); } Ok(()) } /// Apply freeze on reward account to restrict it from going below ED. pub(crate) fn freeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult { T::Currency::set_freeze( &FreezeReason::PoolMinBalance.into(), reward_acc, T::Currency::minimum_balance(), ) } /// Removes the ED freeze on the reward account of `pool_id`. pub fn unfreeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult { T::Currency::thaw(&FreezeReason::PoolMinBalance.into(), reward_acc) } /// 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::reducible_balance(&account, Preservation::Expendable, Fortitude::Polite) < 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::reducible_balance( &account, Preservation::Expendable, Fortitude::Polite ), T::Currency::minimum_balance(), ) } } let mut pools_members = BTreeMap::::new(); let mut pools_members_pending_rewards = BTreeMap::>::new(); let mut all_members = 0u32; let mut total_balance_members = Default::default(); 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. total_balance_members += d.total_balance(); 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 this happens, this is most likely due to an old bug and not a recent code change. // We warn about this in try-runtime checks but do not panic. 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) ); } Ok(()) })?; let mut expected_tvl: BalanceOf = Default::default(); 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", ); expected_tvl += T::Staking::total_stake(&bonded_pool.bonded_account()).unwrap_or_default(); Ok(()) })?; ensure!( MaxPoolMembers::::get().map_or(true, |max| all_members <= max), Error::::MaxPoolMembers ); ensure!( TotalValueLocked::::get() == expected_tvl, "TVL deviates from the actual sum of funds of all Pools." ); ensure!( TotalValueLocked::::get() <= total_balance_members, "TVL must be equal to or less than the total balance of all PoolMembers." ); 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 ); } // Warn if any pool has incorrect ED frozen. We don't want to fail hard as this could be a // result of an intentional ED change. let _ = Self::check_ed_imbalance()?; Ok(()) } /// Check if any pool have an incorrect amount of ED frozen. /// /// This can happen if the ED has changed since the pool was created. #[cfg(any( feature = "try-runtime", feature = "runtime-benchmarks", feature = "fuzzing", test, debug_assertions ))] pub fn check_ed_imbalance() -> Result<(), DispatchError> { let mut failed: u32 = 0; BondedPools::::iter_keys().for_each(|id| { let reward_acc = Self::create_reward_account(id); let frozen_balance = T::Currency::balance_frozen(&FreezeReason::PoolMinBalance.into(), &reward_acc); let expected_frozen_balance = T::Currency::minimum_balance(); if frozen_balance != expected_frozen_balance { failed += 1; log::warn!( "pool {:?} has incorrect ED frozen that can result from change in ED. Expected = {:?}, Actual = {:?}", id, expected_frozen_balance, frozen_balance, ); } }); ensure!(failed == 0, "Some pools do not have correct ED frozen"); 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 { /// Reduces the balances of the [`SubPools`], that belong to the pool involved in the /// slash, to the amount that is defined in the `slashed_unlocking` field of /// [`sp_staking::OnStakingUpdate::on_slash`] /// /// Emits the `PoolsSlashed` event. 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>, total_slashed: BalanceOf, ) { let Some(pool_id) = ReversePoolIdLookup::::get(pool_account) else { return }; // As the slashed account belongs to a `BondedPool` the `TotalValueLocked` decreases and // an event is emitted. TotalValueLocked::::mutate(|tvl| { tvl.defensive_saturating_reduce(total_slashed); }); if let Some(mut sub_pools) = SubPoolsStorage::::get(pool_id) { // set the reduced balance for each of the `SubPools` slashed_unlocking.iter().for_each(|(era, slashed_balance)| { if let Some(pool) = sub_pools.with_era.get_mut(era).defensive() { pool.balance = *slashed_balance; Self::deposit_event(Event::::UnbondingPoolSlashed { era: *era, pool_id, balance: *slashed_balance, }); } }); SubPoolsStorage::::insert(pool_id, sub_pools); } else if !slashed_unlocking.is_empty() { defensive!("Expected SubPools were not found"); } Self::deposit_event(Event::::PoolSlashed { pool_id, balance: slashed_bonded }); } }