Fixes TotalValueLocked out of sync in nomination pools (#3052)

The `TotalLockedValue` storage value in nomination pools pallet may get
out of sync if the staking pallet does implicit withdrawal of unlocking
chunks belonging to a bonded pool stash. This fix is based on a new
method in the `OnStakingUpdate` traits, `on_withdraw`, which allows the
nomination pools pallet to adjust the `TotalLockedValue` every time
there is an implicit or explicit withdrawal from a bonded pool's stash.

This PR also adds a migration that checks and updates the on-chain TVL
if it got out of sync due to the bug this PR fixes.

**Changes to `trait OnStakingUpdate`**

In order for staking to notify the nomination pools pallet that chunks
where withdrew, we add a new method, `on_withdraw` to the
`OnStakingUpdate` trait. The nomination pools pallet filters the
withdraws that are related to bonded pool accounts and updates the
`TotalValueLocked` accordingly.

**Others**
- Adds try-state checks to the EPM/staking e2e tests
- Adds tests for auto withdrawing in the context of nomination pools

**To-do**
- [x] check if we need a migration to fix the current `TotalValueLocked`
(run try-runtime)
- [x] migrations to fix the current on-chain TVL value 

  **Kusama**:
```
TotalValueLocked: 99.4559 kKSM
TotalValueLocked (calculated) 99.4559 kKSM
```
⚠️ **Westend**:
```
TotalValueLocked: 18.4060 kWND
TotalValueLocked (calculated) 18.4050 kWND
```
**Polkadot**: TVL not released yet.

Closes https://github.com/paritytech/polkadot-sdk/issues/3055

---------

Co-authored-by: command-bot <>
Co-authored-by: Ross Bulat <ross@parity.io>
Co-authored-by: Dónal Murray <donal.murray@parity.io>
This commit is contained in:
Gonçalo Pestana
2024-02-08 19:03:22 +01:00
committed by GitHub
parent c36c51cac3
commit aac07af03c
12 changed files with 383 additions and 74 deletions
+16 -26
View File
@@ -1293,27 +1293,6 @@ impl<T: Config> BondedPool<T> {
});
};
}
/// 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<bool, DispatchError> {
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::<T>::mutate(|tvl| {
tvl.saturating_reduce(diff);
});
outcome
}
}
/// A reward pool.
@@ -1733,7 +1712,7 @@ pub mod pallet {
CountedStorageMap<_, Twox64Concat, PoolId, BondedPoolInner<T>>;
/// 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.
/// claimed, the balance comes out of the reward pool. Keyed by the bonded pools account.
#[pallet::storage]
pub type RewardPools<T: Config> = CountedStorageMap<_, Twox64Concat, PoolId, RewardPool<T>>;
@@ -1753,8 +1732,8 @@ pub mod pallet {
/// 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.
/// This is only used for slashing and on automatic withdraw update. In all other instances, the
/// pool id is used, and the accounts are deterministically derived from it.
#[pallet::storage]
pub type ReversePoolIdLookup<T: Config> =
CountedStorageMap<_, Twox64Concat, T::AccountId, PoolId, OptionQuery>;
@@ -2223,7 +2202,7 @@ pub mod pallet {
// 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::<T>::NotDestroying);
pool.withdraw_from_staking(num_slashing_spans)?;
T::Staking::withdraw_unbonded(pool.bonded_account(), num_slashing_spans)?;
Ok(())
}
@@ -2275,7 +2254,8 @@ pub mod pallet {
// 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)?;
let stash_killed =
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?;
// defensive-only: the depositor puts enough funds into the stash so that it will only
// be destroyed when they are leaving.
@@ -3628,4 +3608,14 @@ impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pall
}
Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: slashed_bonded });
}
/// Reduces the overall `TotalValueLocked` if a withdrawal happened for a pool involved in the
/// staking withdraw.
fn on_withdraw(pool_account: &T::AccountId, amount: BalanceOf<T>) {
if ReversePoolIdLookup::<T>::get(pool_account).is_some() {
TotalValueLocked::<T>::mutate(|tvl| {
tvl.saturating_reduce(amount);
});
}
}
}