mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 01:11:10 +00:00
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:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,59 @@ pub mod versioned {
|
||||
>;
|
||||
}
|
||||
|
||||
pub mod unversioned {
|
||||
use super::*;
|
||||
|
||||
/// Checks and updates `TotalValueLocked` if out of sync.
|
||||
pub struct TotalValueLockedSync<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for TotalValueLockedSync<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let migrated = BondedPools::<T>::count();
|
||||
|
||||
// recalcuate the `TotalValueLocked` to compare with the current on-chain TVL which may
|
||||
// be out of sync.
|
||||
let tvl: BalanceOf<T> = helpers::calculate_tvl_by_total_stake::<T>();
|
||||
let onchain_tvl = TotalValueLocked::<T>::get();
|
||||
|
||||
let writes = if tvl != onchain_tvl {
|
||||
TotalValueLocked::<T>::set(tvl);
|
||||
|
||||
log!(
|
||||
info,
|
||||
"on-chain TVL was out of sync, update. Old: {:?}, new: {:?}",
|
||||
onchain_tvl,
|
||||
tvl
|
||||
);
|
||||
|
||||
// writes: onchain version + set total value locked.
|
||||
2
|
||||
} else {
|
||||
log!(info, "on-chain TVL was OK: {:?}", tvl);
|
||||
|
||||
// writes: onchain version write.
|
||||
1
|
||||
};
|
||||
|
||||
// reads: migrated * (BondedPools + Staking::total_stake) + count + onchain
|
||||
// version
|
||||
//
|
||||
// writes: current version + (maybe) TVL
|
||||
T::DbWeight::get()
|
||||
.reads_writes(migrated.saturating_mul(2).saturating_add(2).into(), writes)
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v8 {
|
||||
use super::{v7::V7BondedPoolInner, *};
|
||||
|
||||
@@ -146,6 +199,7 @@ pub(crate) mod v7 {
|
||||
}
|
||||
|
||||
impl<T: Config> V7BondedPool<T> {
|
||||
#[allow(dead_code)]
|
||||
fn bonded_account(&self) -> T::AccountId {
|
||||
Pallet::<T>::create_bonded_account(self.id)
|
||||
}
|
||||
@@ -157,26 +211,12 @@ pub(crate) mod v7 {
|
||||
CountedStorageMap<Pallet<T>, Twox64Concat, PoolId, V7BondedPoolInner<T>>;
|
||||
|
||||
pub struct VersionUncheckedMigrateV6ToV7<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> VersionUncheckedMigrateV6ToV7<T> {
|
||||
fn calculate_tvl_by_total_stake() -> BalanceOf<T> {
|
||||
BondedPools::<T>::iter()
|
||||
.map(|(id, inner)| {
|
||||
T::Staking::total_stake(
|
||||
&V7BondedPool { id, inner: inner.clone() }.bonded_account(),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.reduce(|acc, total_balance| acc + total_balance)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for VersionUncheckedMigrateV6ToV7<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let migrated = BondedPools::<T>::count();
|
||||
// The TVL should be the sum of all the funds that are actively staked and in the
|
||||
// unbonding process of the account of each pool.
|
||||
let tvl: BalanceOf<T> = Self::calculate_tvl_by_total_stake();
|
||||
let tvl: BalanceOf<T> = helpers::calculate_tvl_by_total_stake::<T>();
|
||||
|
||||
TotalValueLocked::<T>::set(tvl);
|
||||
|
||||
@@ -198,7 +238,7 @@ pub(crate) mod v7 {
|
||||
fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
// check that the `TotalValueLocked` written is actually the sum of `total_stake` of the
|
||||
// `BondedPools``
|
||||
let tvl: BalanceOf<T> = Self::calculate_tvl_by_total_stake();
|
||||
let tvl: BalanceOf<T> = helpers::calculate_tvl_by_total_stake::<T>();
|
||||
ensure!(
|
||||
TotalValueLocked::<T>::get() == tvl,
|
||||
"TVL written is not equal to `Staking::total_stake` of all `BondedPools`."
|
||||
@@ -977,3 +1017,17 @@ pub mod v1 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod helpers {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn calculate_tvl_by_total_stake<T: Config>() -> BalanceOf<T> {
|
||||
BondedPools::<T>::iter()
|
||||
.map(|(id, inner)| {
|
||||
T::Staking::total_stake(&BondedPool { id, inner: inner.clone() }.bonded_account())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.reduce(|acc, total_balance| acc + total_balance)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +134,24 @@ impl sp_staking::StakingInterface for StakingMock {
|
||||
|
||||
fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result<bool, DispatchError> {
|
||||
let mut unbonding_map = UnbondingBalanceMap::get();
|
||||
|
||||
// closure to calculate the current unlocking funds across all eras/accounts.
|
||||
let unlocking = |pair: &Vec<(EraIndex, Balance)>| -> Balance {
|
||||
pair.iter()
|
||||
.try_fold(Zero::zero(), |acc: Balance, (_at, amount)| acc.checked_add(*amount))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let staker_map = unbonding_map.get_mut(&who).ok_or("Nothing to unbond")?;
|
||||
let unlocking_before = unlocking(&staker_map);
|
||||
|
||||
let current_era = Self::current_era();
|
||||
|
||||
staker_map.retain(|(unlocking_at, _amount)| *unlocking_at > current_era);
|
||||
|
||||
// if there was a withdrawal, notify the pallet.
|
||||
Pools::on_withdraw(&who, unlocking_before.saturating_sub(unlocking(&staker_map)));
|
||||
|
||||
UnbondingBalanceMap::set(&unbonding_map);
|
||||
Ok(UnbondingBalanceMap::get().is_empty() && BondedBalanceMap::get().is_empty())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user