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.
- pool id: A unique identifier of each pool. Set to u32.
- bonded pool: Tracks the distribution of actively staked funds. See
- reward pool: Tracks rewards earned by actively staked funds. See
- unbonding sub pools: Collection of pools at different phases of the unbonding lifecycle. See
- members: Accounts that are members of pools. See
- 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
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
Payeeaccount 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
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.
An account can stake funds with a nomination pool by calling
After joining a pool, a member can claim rewards by calling
A pool member can also set a
Call::set_claim_permission, to allow
other members to permissionlessly bond or withdraw their rewards by calling
For design docs see the reward pool section.
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::MaxUnbondingdistinct active unbonding requests.
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
- unbonded, or
- 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.
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.
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
- 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
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::withdraw_unbonded. Once a pool is in destroying state, it cannot be reverted to another state.
A pool has 4 administrative roles (see
- 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.
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.
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
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.
As noted, a pool is destroyed once
- First, all members need to fully unbond and withdraw. If the pool state is set to
Destroying, this can happen permissionlessly.
- 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
chillitself to allow the depositor to leave. See
Some notes and common mistakes that wallets/apps wishing to implement this pallet should be aware of:
- 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
bondas 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 ;))
- 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.minNominatorBondlimit. Similar to a normal nominator, who has to first
chillbefore 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.
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
- 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.
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
amount_transferred, we maintain the ratio of bonded balance to points such that:
balance_after_transfer / points_after_transfer == balance_before_transfer / points_before_transfer;
To achieve this, we issue points based on the following:
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
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
See this link for an in-depth explanation of the reward pool mechanism.
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
When a member initiates unbonding it’s claim on the bonded pool (
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
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
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
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.
This section assumes that the slash computation is executed by
pallet_staking::StakingLedger::slash, which passes the information to this pallet via
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: https://github.com/paritytech/substrate/issues/10860
- 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.
palletmodule in each FRAME pallet hosts the most important items needed to construct this pallet.
- Autogenerated weights for pallet_nomination_pools
- A wrapper for bonded pools, with utility functions.
- Pool permissions and state
- Pool commission.
- Pool commission change rate preferences.
- A member in a pool.
- Pool administration roles.
- A reward pool.
- The maximum amount of eras an unbonding pool can exist prior to being merged with the
no_erapool. This is guaranteed to at least be equal to the staking
UnbondingDuration. For improved UX
Config::PostUnbondingPoolsWindowshould be configured to a non-zero value.
- An unbonding pool. This is always mapped with an era.
- How to increase the bond of a member.
- The permission a pool member can set for other accounts to claim rewards on their behalf.
- Possible operations on the configuration values of this pallet.
- A pool’s possible states.
- The log target of this pallet.
- The balance type used by the currency system.
- Type used for unique identifier of each pool.