Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Tvl pool staking #14775

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f435c64
make it mostly work, still needs a bit more
kianenigma Feb 6, 2023
8b4c90d
init
PieWol Aug 14, 2023
fdd0f52
total_balance updated
PieWol Aug 14, 2023
b80f63a
comments
PieWol Aug 14, 2023
1461e0c
Merge remote-tracking branch 'origin/master' into tvl-pool-staking
PieWol Aug 14, 2023
2e8027e
compiling rebase
PieWol Aug 14, 2023
8c69fb8
slashed_total in OnStakingUpdate::on_slash
PieWol Aug 15, 2023
6ead93a
nightly fmt
PieWol Aug 15, 2023
bcffd00
test fix
PieWol Aug 15, 2023
e936de8
remove irrelevant changes
PieWol Aug 15, 2023
6116180
tvl checks on do_try_state
PieWol Aug 15, 2023
cb69b5c
tvl migration
PieWol Aug 15, 2023
5a463c3
fix try_runtime
PieWol Aug 16, 2023
c332c0c
optimize migration
PieWol Aug 16, 2023
ec91ce2
VersionedRuntimeUpgrade
PieWol Aug 16, 2023
db59e96
removed unneccessary logs
PieWol Aug 16, 2023
35027bc
comments improved
PieWol Aug 16, 2023
5098b18
try_runtime total_balance rework
PieWol Aug 17, 2023
2d7a7b1
total_balance simplified
PieWol Aug 18, 2023
672909f
on_slash and do_try_state adjusted
PieWol Aug 18, 2023
6aa682a
test for slash tracking without subpools
PieWol Aug 18, 2023
53e99a0
test fixes and comments.
PieWol Aug 19, 2023
de98dd6
Merge branch 'paritytech:master' into tvl-pool-staking
PieWol Aug 19, 2023
4db0054
points and tvl need to stay in check.
PieWol Aug 19, 2023
4c0dece
undo accidental changes
PieWol Aug 20, 2023
db8f4af
Merge branch 'master' of github.com:paritytech/substrate into tvl-poo…
PieWol Aug 22, 2023
f0dc257
fix migration
PieWol Aug 22, 2023
ed63e99
poolmembers not relevant for TVL
PieWol Aug 25, 2023
69e259e
helper function for staking withdrawal.
PieWol Aug 25, 2023
605838c
Merge branch 'master' into tvl-pool-staking
PieWol Aug 29, 2023
6402f1f
add tvl sanity check via member total_balance
PieWol Aug 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions frame/nomination-pools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ pallet-balances = { version = "4.0.0-dev", path = "../balances" }
sp-tracing = { version = "10.0.0", path = "../../primitives/tracing" }

[features]
default = [ "std" ]
fuzzing = [ "pallet-balances", "sp-tracing" ]
default = ["std"]
# Enable `VersionedRuntimeUpgrade` for the migrations that is currently still experimental.
experimental = [
"frame-support/experimental"
]
fuzzing = ["pallet-balances", "sp-tracing"]

std = [
"codec/std",
"frame-support/std",
Expand Down
151 changes: 120 additions & 31 deletions frame/nomination-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,35 @@ impl<T: Config> PoolMember<T> {
}
}

/// 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(&mut self) -> BalanceOf<T> {
let pool = match BondedPool::<T>::get(self.pool_id).defensive() {
Some(pool) => pool,
None => return Zero::zero(),
};

let active_balance = pool.points_to_balance(self.active_points());

let sub_pools = match SubPoolsStorage::<T>::get(self.pool_id) {
Some(sub_pools) => sub_pools,
None => return active_balance,
};

let unbonding_balance = self.unbonding_eras.iter().fold(
BalanceOf::<T>::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.saturating_add(era_pool.point_to_balance(*unlocked_points))
},
);

active_balance.saturating_add(unbonding_balance)
}

/// Total points of this member, both active and unbonding.
fn total_points(&self) -> BalanceOf<T> {
self.active_points().saturating_add(self.unbonding_points())
Expand Down Expand Up @@ -963,6 +992,7 @@ impl<T: Config> BondedPool<T> {
}

/// Issue points to [`Self`] for `new_funds`.
/// Increase the [`TotalValueLocked`] by `new_funds`.
fn issue(&mut self, new_funds: BalanceOf<T>) -> BalanceOf<T> {
let points_to_issue = self.balance_to_point(new_funds);
self.points = self.points.saturating_add(points_to_issue);
Expand Down Expand Up @@ -1183,9 +1213,8 @@ impl<T: Config> BondedPool<T> {

/// Bond exactly `amount` from `who`'s funds into this pool.
///
/// If the bond type is `Create`, `Staking::bond` is called, and `who`
/// is allowed to be killed. Otherwise, `Staking::bond_extra` is called and `who`
/// cannot be killed.
/// 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(
Expand Down Expand Up @@ -1216,6 +1245,9 @@ impl<T: Config> BondedPool<T> {
// found, we exit early.
BondType::Later => T::Staking::bond_extra(&bonded_account, amount)?,
}
TotalValueLocked::<T>::mutate(|tvl| {
tvl.saturating_accrue(amount);
});

Ok(points_issued)
}
Expand All @@ -1231,6 +1263,19 @@ impl<T: Config> BondedPool<T> {
});
};
}

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.saturating_sub(T::Staking::total_stake(&bonded_account).unwrap_or_default());
TotalValueLocked::<T>::mutate(|tvl| {
tvl.saturating_reduce(diff);
});
outcome
}
}

/// A reward pool.
Expand Down Expand Up @@ -1416,8 +1461,8 @@ impl<T: Config> UnbondPool<T> {
new_points
}

/// Dissolve some points from the unbonding pool, reducing the balance of the pool
/// proportionally.
/// Dissolve some points from the unbonding pool, reducing the balance of the pool and the
/// [`TotalValueLocked`] proportionally.
///
/// This is the opposite of `issue`.
///
Expand Down Expand Up @@ -1504,7 +1549,7 @@ pub mod pallet {
use sp_runtime::Perbill;

/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(5);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -1577,6 +1622,14 @@ pub mod pallet {
type MaxUnbonding: Get<u32>;
}

/// The sum of funds across all pools.
///
/// This might be higher but never lower than the actual sum of the currently unlocking and
/// bonded funds as this is only decreased if a user withdraws unlocked funds or a slash
/// happened.
#[pallet::storage]
pub type TotalValueLocked<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;

/// Minimum amount to bond to join a pool.
#[pallet::storage]
pub type MinJoinBond<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
Expand Down Expand Up @@ -1795,9 +1848,9 @@ pub mod pallet {
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`.
/// 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,
Expand Down Expand Up @@ -2074,7 +2127,7 @@ pub mod pallet {

/// Call `withdraw_unbonded` for the pools account. This call can be made by any account.
///
/// This is useful if their are too many unlocking chunks to call `unbond`, and some
/// 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.
Expand All @@ -2087,10 +2140,12 @@ pub mod pallet {
) -> DispatchResult {
let _ = ensure_signed(origin)?;
let pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::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::<T>::NotDestroying);
T::Staking::withdraw_unbonded(pool.bonded_account(), num_slashing_spans)?;
pool.withdraw_from_staking(num_slashing_spans)?;

Ok(())
}

Expand Down Expand Up @@ -2141,8 +2196,7 @@ pub mod pallet {

// Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the
// `transferrable_balance` is correct.
let stash_killed =
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?;
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.
Expand Down Expand Up @@ -2796,9 +2850,12 @@ impl<T: Config> Pallet<T> {
}

// Equivalent of (current_balance / current_points) * points
balance(u256(current_balance).saturating_mul(u256(points)))
// We check for zero above
.div(current_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.
Expand Down Expand Up @@ -3169,8 +3226,35 @@ impl<T: Config> Pallet<T> {
"depositor must always have MinCreateBond stake in the pool, except for when the \
pool is being destroyed and the depositor is the last member",
);

let expected_tvl: 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();

assert_eq!(
TotalValueLocked::<T>::get(),
expected_tvl,
"TVL deviates from the actual sum of funds of all Pools."
);

let total_balance_members: BalanceOf<T> = PoolMembers::<T>::iter()
.map(|(_, mut member)| member.total_balance())
.reduce(|acc, total_balance| acc + total_balance)
.unwrap_or_default();

assert!(
TotalValueLocked::<T>::get() <= total_balance_members,
"TVL must be equal to or less than the total balance of all PoolMembers."
);
Ok(())
})?;

ensure!(
MaxPoolMembers::<T>::get().map_or(true, |max| all_members <= max),
Error::<T>::MaxPoolMembers
Expand Down Expand Up @@ -3269,25 +3353,30 @@ impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pall
// anything here.
slashed_bonded: BalanceOf<T>,
slashed_unlocking: &BTreeMap<EraIndex, BalanceOf<T>>,
total_slashed: BalanceOf<T>,
) {
if let Some(pool_id) = ReversePoolIdLookup::<T>::get(pool_account) {
let mut sub_pools = match SubPoolsStorage::<T>::get(pool_id).defensive() {
Some(sub_pools) => sub_pools,
None => return,
if let Some(pool_id) = ReversePoolIdLookup::<T>::get(pool_account).defensive() {
match SubPoolsStorage::<T>::get(pool_id) {
Some(mut sub_pools) => {
for (era, slashed_balance) in slashed_unlocking.iter() {
if let Some(pool) = sub_pools.with_era.get_mut(era) {
pool.balance = *slashed_balance;
Self::deposit_event(Event::<T>::UnbondingPoolSlashed {
era: *era,
pool_id,
balance: *slashed_balance,
});
}
}
SubPoolsStorage::<T>::insert(pool_id, sub_pools);
},
None => {},
};
for (era, slashed_balance) in slashed_unlocking.iter() {
if let Some(pool) = sub_pools.with_era.get_mut(era) {
pool.balance = *slashed_balance;
Self::deposit_event(Event::<T>::UnbondingPoolSlashed {
era: *era,
pool_id,
balance: *slashed_balance,
});
}
}

TotalValueLocked::<T>::mutate(|tvl| {
tvl.saturating_reduce(total_slashed);
});
Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: slashed_bonded });
SubPoolsStorage::<T>::insert(pool_id, sub_pools);
}
}
}
99 changes: 99 additions & 0 deletions frame/nomination-pools/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,4 +724,103 @@ pub mod v5 {
Ok(())
}
}

/// This migration accumulates and initializes the [`TotalValueLocked`] for all pools.
pub struct VersionUncheckedMigrateV5ToV6<T>(sp_std::marker::PhantomData<T>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope nobody is us using nomination pools on parachains, but we should mention in the migration that it is using lots of weight and may not be suitable for parachains.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out. How do you think this mention should be done? Just a comment in the code?

impl<T: Config> OnRuntimeUpgrade for VersionUncheckedMigrateV5ToV6<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> = 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();

TotalValueLocked::<T>::set(tvl);

log!(info, "Upgraded {} pools with a TVL of {:?}", migrated, tvl);

// reads: migrated * (BondedPools + Staking::total_stake) + count + onchain
// version
//
// writes: current version + TVL
T::DbWeight::get().reads_writes(migrated.saturating_mul(2).saturating_add(2).into(), 2)
}

#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
ensure!(
PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
"There are undecodable PoolMembers in storage. This migration will not fix that."
);
ensure!(
BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
"There are undecodable BondedPools in storage. This migration will not fix that."
);
ensure!(
SubPoolsStorage::<T>::iter_keys().count() ==
SubPoolsStorage::<T>::iter_values().count(),
"There are undecodable SubPools in storage. This migration will not fix that."
);
ensure!(
Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
"There are undecodable Metadata in storage. This migration will not fix that."
);

Ok(Vec::new())
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
// ensure [`TotalValueLocked`] contains a value greater zero if any instances of
// BondedPools exist.
if !BondedPools::<T>::count().is_zero() {
ensure!(!TotalValueLocked::<T>::get().is_zero(), "tvl written incorrectly");
}

ensure!(
Pallet::<T>::on_chain_storage_version() >= 6,
"nomination-pools::migration::v6: wrong storage version"
);

// These should not have been touched - just in case.
ensure!(
PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
"There are undecodable PoolMembers in storage."
);
ensure!(
BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
"There are undecodable BondedPools in storage."
);
ensure!(
SubPoolsStorage::<T>::iter_keys().count() ==
SubPoolsStorage::<T>::iter_values().count(),
"There are undecodable SubPools in storage."
);
ensure!(
Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
"There are undecodable Metadata in storage."
);

Ok(())
}
}

/// [`VersionUncheckedMigrateV5ToV6`] wrapped in a
/// [`frame_support::migrations::VersionedRuntimeUpgrade`], ensuring the migration is only
/// performed when on-chain version is 5.
#[cfg(feature = "experimental")]
pub type VersionCheckedMigrateV5ToV6<T> = frame_support::migrations::VersionedRuntimeUpgrade<
5,
6,
VersionUncheckedMigrateV5ToV6<T>,
crate::pallet::Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
}
Loading