Nomination pool today does staking in two steps, 1) move the funds to the pool account, 2) stake. After #14540, these steps would become, 1) delegate the funds to the pool account, 2) stake. Migration can be done either lazily or eagerly by moving the locked funds to delegator account. We keep the same points system and we check when user is taking any action how much share of fund they deserve, and apply slashing if necessary. # Delegation We introduce a new staking primitive of `delegation` that is restricted to only to other pallets in the runtime to begin with (not exposed via an extrinsic). **Regular Bond**: An account bonds some fund to `staking-pallet` and nominates a set a of validator. **Delegated Bond**: An account (called `delegator` from hereon) bonds some fund against a delegatee account. The staking pallet locks funds of delegators (in-place) and maintains a ledger of the delegatee accounts. The effective balance of the delegatee account at any point of time is indicated by the locked balance of all its delegators. **Nomination pools**: Each pool member (delegator) bonds to the pool account (delegatee). Nomination pools are responsible for managing the rewards and slashing of the pool. One of the objectives of this new primitive is to keep most of the staking functionalities to be same. We build a pseudo account (delegatee) whose balance is made up from its children (delegators). The old staking primitives (bond, unbond, etc) only now needs to know about how to look up balance for the delegatee. There is also some impact on how slashing would work (would be lazy for delegatee accounts instead of eager slashing) but most of the logic stays same. There are 5 important staking functions that needs to be supported: Bond, Unbond, Withdraw, Reward, Slash. I will compare the regular nomination logic and how the new nomination would work for each of the above mentioned functions. I will also discuss how a higher order pallet which utilises these primitives would work (example `nomination pools`). Eventually, this could be exposed via extrinsic as well and a smart contract on a parachain could implement their own pool logic on top of these staking primitives. ## Storage ### Regular Staking pallet maintains record of active staker using the followed data structures and storage maps. ```rust! /// The ledger of a (bonded) stash. pub struct StakingLedger<T: Config> { /// The stash account whose balance is actually locked and at stake. pub stash: T::AccountId, /// The total amount thats locked (active + unlocking) pub total: BalanceOf<T>, /// Total amount that is eligible for reward (and slash) in forthcoming rounds pub active: BalanceOf<T>, /// Any balance that is becoming free pub unlocking: Vec<UnlockChunk<BalanceOf<T>>>, } pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> { /// Amount of funds to be unlocked. value: Balance, /// Era number at which point it'll be unlocked. era: EraIndex, } ``` ### Delegatee These would be the new storage items we would need to build the delegatee account. ```rust! pub type Delegators<T: Config> = StorageDoubleMap< _, Twox64Concat, T::AccountId, // delegatee Twox64Concat, T::AccountId, // delegator BalanceOf<T>, // held balance ValueQuery, >; /// Ledger of Delegatee Account pub type Delegatee<T: Config> = CountedStorageMap< _, Twox64Concat, T::AccountId, // delegatee DelegationAggregate<T>, ValueQuery, >; pub struct DelegationAggregate<T: Config> { pub balance: BalanceOf<T>, // locked so we guarantee that money exists. pub pending_slash: BalanceOf<T>, // slashes that are pending to be applied. // pub distribution: Vec<(T::AccountId, BalanceOf<T>)>, // distribution of child accounts who make up the delegatee balance. } impl DelegationAggregate<T> { /// delegatee account is frozen until slash is applied. pub fn is_frozen(&self) -> bool { self.pending_slash >= self.balance } /// Effective balance of the delegatee account. pub fn balance(&self) -> BalanceOf<T> { self.balance - self.pending_slash } } ``` ## Bond #### Direct Stake **Signature: `fn bond(staker: AccountId, value: Balance, payee: AccountId)`** - Creates a new `StakingLedger` for the staker (could be nominator or validator). - Sets lock on `value`. - Reward is paid out to the payee account. **Signature: `fn bond_extra(staker: AccountId, extra: Balance)`** - Updates `StakingLedger` of the staker adding the `extra` to active balance. - Updates lock on staker account with the new ledger balance. #### Delegation **Signature: `fn delegated_bond(delegator: AccountId, delegatee: AccountId, value: Balance, payee: AccountId)`** and, **Signature: `fn delegated_bond_extra(delegator: AccountId, delegatee: AccountId, value: Balance)`** - A delegator delegates `value` to a delegatee. - New entry added to Delegatee with `DelegationAggregate { balance: value, pending_slash: 0 }`. - Locks `value` in-place on `delegator` account. - Calls internal function `bond(delegatee, value, payee)` (or bond_extra if delegatee already exists) which should be refactored to do the following - Creates a new `StakingLedger` for the delegatee looking up balance from `DelegationAggregate`. - No need to lock as this is done already by the caller. ## Unbond #### Direct Stake **Signature: `fn unbond(staker: AccountId, value: Balance)`** - Reduce active balance from `StakingLedger`. - Add `value` to `StakingLedger.UnlockingChunks` mapped to the unlocking era. - No locks are updated. #### Delegated Stake **Signature: `fn delegated_unbond(delegatee: AccountId, delegator: AccountId, value: Balance)`** - Same as before, we reduce active balance from `StakingLedger`. - Add `value` to `StakingLedger.UnlockingChunks`, this time we map it to the unlocking era as well as delegator. This is so that we can partial withdraw one deleagtor at a time. - No locks are updated. ## Withdraw #### Direct Stake **Signature: `fn withdraw_unbonded(staker: AccountId, num_slashing_spans: u32)`** - Consolidate all unlocking chunks of `StakingLedger` where `current_era` > `mapped_era`. - Reduce `ledger.total` with unlocking balance. - Update lock on staker account with new `ledger.total`. #### Delegated Stake **Signature `fn delegated_withdraw(delegatee: AccountId, delegator: AccountId)`** - Consolidate all unlocking chunks of `StakingLedger` of delegatee for the passed delegator. - Reduce `ledger.total` with unlocking balance. - Get old balance from storage `Delegators`. Update lock on `delegator` account with `old_balance - unlocking_balance` and update new balance in Delegators. #### Migrations needed - Will need to add an additional field to `StakingLedger.UnlockChunk` for the `delegator`. ```rust! pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> { /// delegator account, `None` for regular stakers, `Some` for delegatees. delegator: Option<AccountId>, /// Amount of funds to be unlocked. value: Balance, /// Era number at which point it'll be unlocked. era: EraIndex, } ``` ## Reward #### Direct Stake Reward is paid out to the `RewardDestination` passed on the `Staking::bond()`. A permissionless call to `payout_stakers` pays out all the nominators for the validator who earned era rewards. #### Delegated Stake Nothing changes except may be we should not allow certain `RewardDestinations` for Delegatees? #### Nomination Pools `NominationPool` implementation should always set a reward destination as a separate `reward_account` associated to the pool. When a member claims payout, the reward is paid out to member account based on the calculation of their `share of points in the pool` * `change in rewards since last claim`. ## Slashing #### Direct Stake - Eager slashing. - StakingLedger is slashed. - Locked balance is slashed. #### Delegated Stake When delegatee is slashed, staking pallet needs to apply proportional slashing to all delegator locked balance. But this can mess up the `NominationPool` (or some other higher order pallet/contract). Instead we leave this responsibility upto the `NominationPool` to figure out how to apply slashing (lazily). - Add balance to be slashed to `pending_slash` in `DelegationAggregate`. - Ensure real balance of delegatee account (`balance` - `pending_slash`) never drops below zero. **Signature: `fn delegate_slash(delegatee: AccountId, delegator: AccountId, value: Balance)`** - Try to slash locked balance of delegator with value. - If successful, update `StakingLedger` by reducing `balance` and `pending_slash`. - This continuously applied for other delegators until `pending_slash` is zero. - If `pending_slash` is zero, account is unfrozen and can be used again for staking activities. ## Nomination Pool slashing We do something very similar to how we payout rewards to [pool delegators](https://hackmd.io/PFGn6wI5TbCmBYoEA_f2Uw). ### Concerns/questions - Slashing with sub pools? - If pool owners receives commission for managing pool, should they get slashed similarly to commission ratio first, before spreading the slashes to other delegators? - We still need to maintain notion of points and pool bonded points for applying slashes correctly? For withdraws though we don't need to use points since balance is already slashed. Seems a bit hacky and need more solid proof on how this will work. ### State #### Pool - **last_recorded_slash_counter**: Updated only when pool points change. - **last_recorded_total_applied_slashes** : Updated only when pool points change. Summation of `total_applied_slashes` and monotonically increasing. - **total_applied_slashes** : Sum of all slashes that are applied already for the pool. Unit is `Balance`. #### Delegator - **last_applied_slash_counter** ### Pseudo code ```rust fn slash_counter(pool) -> Counter { slashes_since_last_record = pool.pending_slash + pool.total_applied_slashes - pool.last_recorded_total_slashes; pool.last_recorded_slash_counter + slashes_since_last_record / pool.bonded_points } ``` ```rust fn pending_slash(delegator) -> Balance { delegator.points * (slash_counter(delegator.pool) - delegator.last_applied_slash_counter) } ``` ### Updates Slash counter is updated and pending slashes for a delegator is applied on following actions: - claim_rewards: This should first apply pending slash before rewards are claimed. - join: Since pool points change, pool slash counters are updated and delegator slash counter is set to current slash counter. - bond_extra: Apply slash, update pool slash counters, update delegator slash counter. - unbond: - withdraw: - apply_slash: new call. ## Braindump on how this can be done in a general way so that delegated staking calls are exposed by the runtime - Delegatee registers that it is a delegatee. - Delegator delegates to delegatee, and update bond (bond_extra is permissionless for delegatees). - Only delegatee can unbond or withdraw. Another idea: - May be have a delegation pallet that maintains all delegation. And a balance_provider in staking where set delegation pallet to be balance_provider.