mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 13:21:01 +00:00
Tvl pool staking (#1322)
What does this PR do? - Introduced the TotalValueLocked storage for nomination-pools. - introduced a slashing api in mock.rs - additional test for tracking a slashing event towards a pool without sub-pools - migration for the nomination-pools (V6 to V7) with `VersionedMigration` Why are these changes needed? this is the continuation of the work by @kianenigma in this [PR](https://github.com/paritytech/substrate/pull/13319) How were these changes implemented and what do they affect? - It's an extra StorageValue that's modified whenever funds flow in or out of staking for any of the `bonded_account` of `BondedPools` - The `PoolSlashed`event is now emitted even when no `SubPools` are found Closes https://github.com/paritytech/polkadot-sdk/issues/155 KSM: HHEEgVzcqL3kCXgsxSfJMbsTy8dxoTctuXtpY94n4s8F4pS --------- Co-authored-by: Liam Aharon <liam.aharon@hotmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: Ankan <10196091+Ank4n@users.noreply.github.com> Co-authored-by: Ankan <ankan.anurag@gmail.com> Co-authored-by: command-bot <>
This commit is contained in:
@@ -1508,6 +1508,7 @@ pub mod migrations {
|
||||
paras_registrar::migration::VersionCheckedMigrateToV1<Runtime, ()>,
|
||||
pallet_nomination_pools::migration::versioned_migrations::V5toV6<Runtime>,
|
||||
pallet_referenda::migration::v1::MigrateV0ToV1<Runtime, ()>,
|
||||
pallet_nomination_pools::migration::versioned_migrations::V6ToV7<Runtime>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -538,6 +538,31 @@ 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(&self) -> BalanceOf<T> {
|
||||
let pool = BondedPool::<T>::get(self.pool_id).unwrap();
|
||||
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 + (era_pool.point_to_balance(*unlocked_points))
|
||||
},
|
||||
);
|
||||
|
||||
active_balance + 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())
|
||||
@@ -1189,11 +1214,11 @@ impl<T: Config> BondedPool<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bond exactly `amount` from `who`'s funds into this pool.
|
||||
/// Bond exactly `amount` from `who`'s funds into this pool. Increases the [`TotalValueLocked`]
|
||||
/// by `amount`.
|
||||
///
|
||||
/// 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(
|
||||
@@ -1224,6 +1249,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)
|
||||
}
|
||||
@@ -1239,6 +1267,27 @@ 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.
|
||||
@@ -1437,9 +1486,7 @@ impl<T: Config> UnbondPool<T> {
|
||||
}
|
||||
|
||||
/// Dissolve some points from the unbonding pool, reducing the balance of the pool
|
||||
/// proportionally.
|
||||
///
|
||||
/// This is the opposite of `issue`.
|
||||
/// proportionally. This is the opposite of `issue`.
|
||||
///
|
||||
/// Returns the actual amount of `Balance` that was removed from the pool.
|
||||
fn dissolve(&mut self, points: BalanceOf<T>) -> BalanceOf<T> {
|
||||
@@ -1525,7 +1572,7 @@ pub mod pallet {
|
||||
use sp_runtime::Perbill;
|
||||
|
||||
/// The current storage version.
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(7);
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
@@ -1602,6 +1649,14 @@ pub mod pallet {
|
||||
type MaxUnbonding: Get<u32>;
|
||||
}
|
||||
|
||||
/// The sum of funds across all pools.
|
||||
///
|
||||
/// This might be lower but never higher than the sum of `total_balance` of all [`PoolMembers`]
|
||||
/// because calling `pool_withdraw_unbonded` might decrease the total stake of the pool's
|
||||
/// `bonded_account` without adjusting the pallet-internal `UnbondingPool`'s.
|
||||
#[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>;
|
||||
@@ -1825,9 +1880,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,
|
||||
@@ -2114,7 +2169,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.
|
||||
@@ -2127,10 +2182,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(())
|
||||
}
|
||||
|
||||
@@ -2180,9 +2237,8 @@ pub mod pallet {
|
||||
ensure!(!withdrawn_points.is_empty(), Error::<T>::CannotWithdrawAny);
|
||||
|
||||
// Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the
|
||||
// `transferable_balance` is correct.
|
||||
let stash_killed =
|
||||
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?;
|
||||
// `transferrable_balance` is correct.
|
||||
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.
|
||||
@@ -2846,12 +2902,9 @@ impl<T: Config> Pallet<T> {
|
||||
},
|
||||
(false, false) => {
|
||||
// Equivalent to (current_points / current_balance) * new_funds
|
||||
balance(
|
||||
u256(current_points)
|
||||
.saturating_mul(u256(new_funds))
|
||||
balance(u256(current_points).saturating_mul(u256(new_funds)))
|
||||
// We check for zero above
|
||||
.div(u256(current_balance)),
|
||||
)
|
||||
.div(current_balance)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2871,9 +2924,12 @@ impl<T: Config> Pallet<T> {
|
||||
}
|
||||
|
||||
// Equivalent of (current_balance / current_points) * points
|
||||
balance(u256(current_balance).saturating_mul(u256(points)))
|
||||
balance(
|
||||
u256(current_balance)
|
||||
.saturating_mul(u256(points))
|
||||
// We check for zero above
|
||||
.div(current_points)
|
||||
.div(u256(current_points)),
|
||||
)
|
||||
}
|
||||
|
||||
/// If the member has some rewards, transfer a payout from the reward pool to the member.
|
||||
@@ -3242,6 +3298,7 @@ impl<T: Config> Pallet<T> {
|
||||
let mut pools_members = BTreeMap::<PoolId, u32>::new();
|
||||
let mut pools_members_pending_rewards = BTreeMap::<PoolId, BalanceOf<T>>::new();
|
||||
let mut all_members = 0u32;
|
||||
let mut total_balance_members = Default::default();
|
||||
PoolMembers::<T>::iter().try_for_each(|(_, d)| -> Result<(), TryRuntimeError> {
|
||||
let bonded_pool = BondedPools::<T>::get(d.pool_id).unwrap();
|
||||
ensure!(!d.total_points().is_zero(), "No member should have zero points");
|
||||
@@ -3257,6 +3314,7 @@ impl<T: Config> Pallet<T> {
|
||||
let pending_rewards = d.pending_rewards(current_rc).unwrap();
|
||||
*pools_members_pending_rewards.entry(d.pool_id).or_default() += pending_rewards;
|
||||
} // else this pool has been heavily slashed and cannot have any rewards anymore.
|
||||
total_balance_members += d.total_balance();
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -3280,6 +3338,7 @@ impl<T: Config> Pallet<T> {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let mut expected_tvl: BalanceOf<T> = Default::default();
|
||||
BondedPools::<T>::iter().try_for_each(|(id, inner)| -> Result<(), TryRuntimeError> {
|
||||
let bonded_pool = BondedPool { id, inner };
|
||||
ensure!(
|
||||
@@ -3300,13 +3359,28 @@ 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",
|
||||
);
|
||||
|
||||
expected_tvl +=
|
||||
T::Staking::total_stake(&bonded_pool.bonded_account()).unwrap_or_default();
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
ensure!(
|
||||
MaxPoolMembers::<T>::get().map_or(true, |max| all_members <= max),
|
||||
Error::<T>::MaxPoolMembers
|
||||
);
|
||||
|
||||
ensure!(
|
||||
TotalValueLocked::<T>::get() == expected_tvl,
|
||||
"TVL deviates from the actual sum of funds of all Pools."
|
||||
);
|
||||
|
||||
ensure!(
|
||||
TotalValueLocked::<T>::get() <= total_balance_members,
|
||||
"TVL must be equal to or less than the total balance of all PoolMembers."
|
||||
);
|
||||
|
||||
if level <= 1 {
|
||||
return Ok(())
|
||||
}
|
||||
@@ -3424,20 +3498,30 @@ impl<T: Config> Pallet<T> {
|
||||
}
|
||||
|
||||
impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pallet<T> {
|
||||
/// Reduces the balances of the [`SubPools`], that belong to the pool involved in the
|
||||
/// slash, to the amount that is defined in the `slashed_unlocking` field of
|
||||
/// [`sp_staking::OnStakingUpdate::on_slash`]
|
||||
///
|
||||
/// Emits the `PoolsSlashed` event.
|
||||
fn on_slash(
|
||||
pool_account: &T::AccountId,
|
||||
// Bonded balance is always read directly from staking, therefore we don't need to update
|
||||
// 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,
|
||||
};
|
||||
for (era, slashed_balance) in slashed_unlocking.iter() {
|
||||
if let Some(pool) = sub_pools.with_era.get_mut(era) {
|
||||
let Some(pool_id) = ReversePoolIdLookup::<T>::get(pool_account) else { return };
|
||||
// As the slashed account belongs to a `BondedPool` the `TotalValueLocked` decreases and
|
||||
// an event is emitted.
|
||||
TotalValueLocked::<T>::mutate(|tvl| {
|
||||
tvl.defensive_saturating_reduce(total_slashed);
|
||||
});
|
||||
|
||||
if let Some(mut sub_pools) = SubPoolsStorage::<T>::get(pool_id) {
|
||||
// set the reduced balance for each of the `SubPools`
|
||||
slashed_unlocking.iter().for_each(|(era, slashed_balance)| {
|
||||
if let Some(pool) = sub_pools.with_era.get_mut(era).defensive() {
|
||||
pool.balance = *slashed_balance;
|
||||
Self::deposit_event(Event::<T>::UnbondingPoolSlashed {
|
||||
era: *era,
|
||||
@@ -3445,10 +3529,11 @@ impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pall
|
||||
balance: *slashed_balance,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: slashed_bonded });
|
||||
});
|
||||
SubPoolsStorage::<T>::insert(pool_id, sub_pools);
|
||||
}
|
||||
} else if !slashed_unlocking.is_empty() {
|
||||
defensive!("Expected SubPools were not found");
|
||||
}
|
||||
Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: slashed_bonded });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,16 @@ use sp_runtime::TryRuntimeError;
|
||||
pub mod versioned_migrations {
|
||||
use super::*;
|
||||
|
||||
/// Migration V6 to V7 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring
|
||||
/// the migration is only performed when on-chain version is 6.
|
||||
pub type V6ToV7<T> = frame_support::migrations::VersionedMigration<
|
||||
6,
|
||||
7,
|
||||
v7::VersionUncheckedMigrateV6ToV7<T>,
|
||||
crate::pallet::Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
/// Wrapper over `MigrateToV6` with convenience version checks.
|
||||
pub type V5toV6<T> = frame_support::migrations::VersionedMigration<
|
||||
5,
|
||||
@@ -37,6 +47,83 @@ pub mod versioned_migrations {
|
||||
>;
|
||||
}
|
||||
|
||||
/// This migration accumulates and initializes the [`TotalValueLocked`] for all pools.
|
||||
///
|
||||
/// WARNING: This migration works under the assumption that the [`BondedPools`] cannot be inflated
|
||||
/// arbitrarily. Otherwise this migration could fail due to too high weight.
|
||||
mod v7 {
|
||||
use super::*;
|
||||
|
||||
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(
|
||||
&BondedPool { 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();
|
||||
|
||||
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> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
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();
|
||||
ensure!(
|
||||
TotalValueLocked::<T>::get() == tvl,
|
||||
"TVL written is not equal to `Staking::total_stake` of all `BondedPools`."
|
||||
);
|
||||
|
||||
// calculate the sum of `total_balance` of all `PoolMember` as the upper bound for the
|
||||
// `TotalValueLocked`.
|
||||
let total_balance_members: BalanceOf<T> = PoolMembers::<T>::iter()
|
||||
.map(|(_, member)| member.total_balance())
|
||||
.reduce(|acc, total_balance| acc + total_balance)
|
||||
.unwrap_or_default();
|
||||
|
||||
ensure!(
|
||||
TotalValueLocked::<T>::get() <= total_balance_members,
|
||||
"TVL is greater than the balance of all PoolMembers."
|
||||
);
|
||||
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 7,
|
||||
"nomination-pools::migration::v7: wrong storage version"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v6 {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::{self as pools};
|
||||
use frame_support::{assert_ok, parameter_types, traits::fungible::Mutate, PalletId};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::{BuildStorage, FixedU128};
|
||||
use sp_staking::Stake;
|
||||
use sp_staking::{OnStakingUpdate, Stake};
|
||||
|
||||
pub type BlockNumber = u64;
|
||||
pub type AccountId = u128;
|
||||
@@ -46,7 +46,8 @@ parameter_types! {
|
||||
pub static CurrentEra: EraIndex = 0;
|
||||
pub static BondingDuration: EraIndex = 3;
|
||||
pub storage BondedBalanceMap: BTreeMap<AccountId, Balance> = Default::default();
|
||||
pub storage UnbondingBalanceMap: BTreeMap<AccountId, Balance> = Default::default();
|
||||
// map from a user to a vec of eras and amounts being unlocked in each era.
|
||||
pub storage UnbondingBalanceMap: BTreeMap<AccountId, Vec<(EraIndex, Balance)>> = Default::default();
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub static MaxUnbonding: u32 = 8;
|
||||
pub static StakingMinBond: Balance = 10;
|
||||
@@ -60,6 +61,19 @@ impl StakingMock {
|
||||
x.insert(who, bonded);
|
||||
BondedBalanceMap::set(&x)
|
||||
}
|
||||
/// Mimics a slash towards a pool specified by `pool_id`.
|
||||
/// This reduces the bonded balance of a pool by `amount` and calls [`Pools::on_slash`] to
|
||||
/// enact changes in the nomination-pool pallet.
|
||||
///
|
||||
/// Does not modify any [`SubPools`] of the pool as [`Default::default`] is passed for
|
||||
/// `slashed_unlocking`.
|
||||
pub fn slash_by(pool_id: PoolId, amount: Balance) {
|
||||
let acc = Pools::create_bonded_account(pool_id);
|
||||
let bonded = BondedBalanceMap::get();
|
||||
let pre_total = bonded.get(&acc).unwrap();
|
||||
Self::set_bonded_balance(acc, pre_total - amount);
|
||||
Pools::on_slash(&acc, pre_total - amount, &Default::default(), amount);
|
||||
}
|
||||
}
|
||||
|
||||
impl sp_staking::StakingInterface for StakingMock {
|
||||
@@ -105,8 +119,11 @@ impl sp_staking::StakingInterface for StakingMock {
|
||||
let mut x = BondedBalanceMap::get();
|
||||
*x.get_mut(who).unwrap() = x.get_mut(who).unwrap().saturating_sub(amount);
|
||||
BondedBalanceMap::set(&x);
|
||||
|
||||
let era = Self::current_era();
|
||||
let unlocking_at = era + Self::bonding_duration();
|
||||
let mut y = UnbondingBalanceMap::get();
|
||||
*y.entry(*who).or_insert(Self::Balance::zero()) += amount;
|
||||
y.entry(*who).or_insert(Default::default()).push((unlocking_at, amount));
|
||||
UnbondingBalanceMap::set(&y);
|
||||
Ok(())
|
||||
}
|
||||
@@ -116,11 +133,13 @@ impl sp_staking::StakingInterface for StakingMock {
|
||||
}
|
||||
|
||||
fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result<bool, DispatchError> {
|
||||
// Simulates removing unlocking chunks and only having the bonded balance locked
|
||||
let mut x = UnbondingBalanceMap::get();
|
||||
x.remove(&who);
|
||||
UnbondingBalanceMap::set(&x);
|
||||
let mut unbonding_map = UnbondingBalanceMap::get();
|
||||
let staker_map = unbonding_map.get_mut(&who).ok_or("Nothing to unbond")?;
|
||||
|
||||
let current_era = Self::current_era();
|
||||
staker_map.retain(|(unlocking_at, _amount)| *unlocking_at > current_era);
|
||||
|
||||
UnbondingBalanceMap::set(&unbonding_map);
|
||||
Ok(UnbondingBalanceMap::get().is_empty() && BondedBalanceMap::get().is_empty())
|
||||
}
|
||||
|
||||
@@ -144,14 +163,17 @@ impl sp_staking::StakingInterface for StakingMock {
|
||||
}
|
||||
|
||||
fn stake(who: &Self::AccountId) -> Result<Stake<Balance>, DispatchError> {
|
||||
match (
|
||||
UnbondingBalanceMap::get().get(who).copied(),
|
||||
BondedBalanceMap::get().get(who).copied(),
|
||||
) {
|
||||
match (UnbondingBalanceMap::get().get(who), BondedBalanceMap::get().get(who).copied()) {
|
||||
(None, None) => Err(DispatchError::Other("balance not found")),
|
||||
(Some(v), None) => Ok(Stake { total: v, active: 0 }),
|
||||
(Some(v), None) => Ok(Stake {
|
||||
total: v.into_iter().fold(0u128, |acc, &x| acc.saturating_add(x.1)),
|
||||
active: 0,
|
||||
}),
|
||||
(None, Some(v)) => Ok(Stake { total: v, active: v }),
|
||||
(Some(a), Some(b)) => Ok(Stake { total: a + b, active: b }),
|
||||
(Some(a), Some(b)) => Ok(Stake {
|
||||
total: a.into_iter().fold(0u128, |acc, &x| acc.saturating_add(x.1)) + b,
|
||||
active: b,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ fn test_setup_works() {
|
||||
assert_eq!(StakingMock::bonding_duration(), 3);
|
||||
assert!(Metadata::<T>::contains_key(1));
|
||||
|
||||
// initial member.
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 10);
|
||||
|
||||
let last_pool = LastPoolId::<Runtime>::get();
|
||||
assert_eq!(
|
||||
BondedPool::<Runtime>::get(last_pool).unwrap(),
|
||||
@@ -218,10 +221,7 @@ mod bonded_pool {
|
||||
|
||||
// slash half of the pool's balance. expected result of `fn api_points_to_balance`
|
||||
// to be 1/2 of the pool's balance.
|
||||
StakingMock::set_bonded_balance(
|
||||
default_bonded_account(),
|
||||
Pools::depositor_min_bond() / 2,
|
||||
);
|
||||
StakingMock::slash_by(1, Pools::depositor_min_bond() / 2);
|
||||
assert_eq!(Pallet::<Runtime>::api_points_to_balance(1, 10), 5);
|
||||
|
||||
// if pool does not exist, points to balance ratio is 0.
|
||||
@@ -238,10 +238,7 @@ mod bonded_pool {
|
||||
|
||||
// slash half of the pool's balance. expect result of `fn api_balance_to_points`
|
||||
// to be 2 * of the balance to add to the pool.
|
||||
StakingMock::set_bonded_balance(
|
||||
default_bonded_account(),
|
||||
Pools::depositor_min_bond() / 2,
|
||||
);
|
||||
StakingMock::slash_by(1, Pools::depositor_min_bond() / 2);
|
||||
assert_eq!(Pallet::<Runtime>::api_balance_to_points(1, 10), 20);
|
||||
|
||||
// if pool does not exist, balance to points ratio is 0.
|
||||
@@ -637,12 +634,12 @@ mod join {
|
||||
// Given
|
||||
Currency::set_balance(&11, ExistentialDeposit::get() + 2);
|
||||
assert!(!PoolMembers::<Runtime>::contains_key(11));
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 10);
|
||||
|
||||
// When
|
||||
assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1));
|
||||
|
||||
// Then
|
||||
|
||||
assert_eq!(
|
||||
pool_events_since_last_call(),
|
||||
vec![
|
||||
@@ -651,6 +648,7 @@ mod join {
|
||||
Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true },
|
||||
]
|
||||
);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 12);
|
||||
|
||||
assert_eq!(
|
||||
PoolMembers::<Runtime>::get(11).unwrap(),
|
||||
@@ -660,7 +658,7 @@ mod join {
|
||||
|
||||
// Given
|
||||
// The bonded balance is slashed in half
|
||||
StakingMock::set_bonded_balance(Pools::create_bonded_account(1), 6);
|
||||
StakingMock::slash_by(1, 6);
|
||||
|
||||
// And
|
||||
Currency::set_balance(&12, ExistentialDeposit::get() + 12);
|
||||
@@ -672,8 +670,12 @@ mod join {
|
||||
// Then
|
||||
assert_eq!(
|
||||
pool_events_since_last_call(),
|
||||
vec![Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true }]
|
||||
vec![
|
||||
Event::PoolSlashed { pool_id: 1, balance: 6 },
|
||||
Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true }
|
||||
]
|
||||
);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 18);
|
||||
|
||||
assert_eq!(
|
||||
PoolMembers::<Runtime>::get(12).unwrap(),
|
||||
@@ -2359,11 +2361,15 @@ mod unbond {
|
||||
.min_join_bond(10)
|
||||
.add_members(vec![(20, 20)])
|
||||
.build_and_execute(|| {
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 30);
|
||||
// can unbond to above limit
|
||||
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5));
|
||||
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().active_points(), 15);
|
||||
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_points(), 5);
|
||||
|
||||
// tvl remains unchanged.
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 30);
|
||||
|
||||
// cannot go to below 10:
|
||||
assert_noop!(
|
||||
Pools::unbond(RuntimeOrigin::signed(20), 20, 10),
|
||||
@@ -2669,8 +2675,9 @@ mod unbond {
|
||||
.add_members(vec![(40, 40), (550, 550)])
|
||||
.build_and_execute(|| {
|
||||
let ed = Currency::minimum_balance();
|
||||
// Given a slash from 600 -> 100
|
||||
StakingMock::set_bonded_balance(default_bonded_account(), 100);
|
||||
// Given a slash from 600 -> 500
|
||||
StakingMock::slash_by(1, 500);
|
||||
|
||||
// and unclaimed rewards of 600.
|
||||
Currency::set_balance(&default_reward_account(), ed + 600);
|
||||
|
||||
@@ -2702,8 +2709,9 @@ mod unbond {
|
||||
Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true },
|
||||
Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true },
|
||||
Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true },
|
||||
Event::PoolSlashed { pool_id: 1, balance: 100 },
|
||||
Event::PaidOut { member: 40, pool_id: 1, payout: 40 },
|
||||
Event::Unbonded { member: 40, pool_id: 1, points: 6, balance: 6, era: 3 }
|
||||
Event::Unbonded { member: 40, pool_id: 1, balance: 6, points: 6, era: 3 }
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2863,6 +2871,7 @@ mod unbond {
|
||||
);
|
||||
|
||||
// When the root kicks then its ok
|
||||
// Account with ID 100 is kicked.
|
||||
assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(900), 100));
|
||||
|
||||
assert_eq!(
|
||||
@@ -2883,6 +2892,7 @@ mod unbond {
|
||||
);
|
||||
|
||||
// When the bouncer kicks then its ok
|
||||
// Account with ID 200 is kicked.
|
||||
assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(902), 200));
|
||||
|
||||
assert_eq!(
|
||||
@@ -2921,7 +2931,7 @@ mod unbond {
|
||||
);
|
||||
assert_eq!(
|
||||
*UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(),
|
||||
100 + 200
|
||||
vec![(3, 100), (3, 200)],
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -3020,7 +3030,10 @@ mod unbond {
|
||||
}
|
||||
);
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0);
|
||||
assert_eq!(*UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), 10);
|
||||
assert_eq!(
|
||||
*UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(),
|
||||
vec![(6, 10)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3298,7 +3311,7 @@ mod unbond {
|
||||
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 0);
|
||||
|
||||
// slash the default pool
|
||||
StakingMock::set_bonded_balance(Pools::create_bonded_account(1), 5);
|
||||
StakingMock::slash_by(1, 5);
|
||||
|
||||
// cannot unbond even 7, because the value of shares is now less.
|
||||
assert_noop!(
|
||||
@@ -3368,21 +3381,58 @@ mod pool_withdraw_unbonded {
|
||||
|
||||
#[test]
|
||||
fn pool_withdraw_unbonded_works() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Given 10 unbonded directly against the pool account
|
||||
assert_ok!(StakingMock::unbond(&default_bonded_account(), 5));
|
||||
// and the pool account only has 10 balance
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(5));
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(10));
|
||||
assert_eq!(Currency::free_balance(&default_bonded_account()), 10);
|
||||
ExtBuilder::default().add_members(vec![(20, 10)]).build_and_execute(|| {
|
||||
// Given 10 unbond'ed directly against the pool account
|
||||
|
||||
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5));
|
||||
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15));
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(20));
|
||||
assert_eq!(Balances::free_balance(&default_bonded_account()), 20);
|
||||
|
||||
// When
|
||||
CurrentEra::set(StakingMock::current_era() + StakingMock::bonding_duration() + 1);
|
||||
assert_ok!(Pools::pool_withdraw_unbonded(RuntimeOrigin::signed(10), 1, 0));
|
||||
|
||||
// Then there unbonding balance is no longer locked
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(5));
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(5));
|
||||
assert_eq!(Currency::free_balance(&default_bonded_account()), 10);
|
||||
// Then their unbonding balance is no longer locked
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15));
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(15));
|
||||
assert_eq!(Balances::free_balance(&default_bonded_account()), 20);
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn pool_withdraw_unbonded_creates_tvl_diff() {
|
||||
ExtBuilder::default().add_members(vec![(20, 10)]).build_and_execute(|| {
|
||||
// Given 10 unbond'ed directly against the pool account
|
||||
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5));
|
||||
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15));
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(20));
|
||||
assert_eq!(Balances::free_balance(&default_bonded_account()), 20);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 20);
|
||||
|
||||
// When
|
||||
CurrentEra::set(StakingMock::current_era() + StakingMock::bonding_duration() + 1);
|
||||
assert_ok!(Pools::pool_withdraw_unbonded(RuntimeOrigin::signed(10), 1, 0));
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 15);
|
||||
|
||||
let member_balance = PoolMembers::<T>::iter()
|
||||
.map(|(_, member)| member.total_balance())
|
||||
.reduce(|acc, total_balance| acc + total_balance)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Then their unbonding balance is no longer locked
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15));
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(15));
|
||||
assert_eq!(Currency::free_balance(&default_bonded_account()), 20);
|
||||
|
||||
// The difference between TVL and member_balance is exactly the difference between
|
||||
// `total_stake` and the `free_balance`.
|
||||
// This relation is not guaranteed in the wild as arbitrary transfers towards
|
||||
// `free_balance` can be made to the pool that are not accounted for.
|
||||
let non_locked_balance = Balances::free_balance(&default_bonded_account()) -
|
||||
StakingMock::total_stake(&default_bonded_account()).unwrap();
|
||||
assert_eq!(member_balance, TotalValueLocked::<T>::get() + non_locked_balance);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3412,24 +3462,33 @@ mod withdraw_unbonded {
|
||||
let unbond_pool = sub_pools.with_era.get_mut(&3).unwrap();
|
||||
// Sanity check
|
||||
assert_eq!(*unbond_pool, UnbondPool { points: 550 + 40, balance: 550 + 40 });
|
||||
assert_eq!(TotalValueLocked::<Runtime>::get(), 600);
|
||||
|
||||
// Simulate a slash to the pool with_era(current_era), decreasing the balance by
|
||||
// half
|
||||
{
|
||||
unbond_pool.balance /= 2; // 295
|
||||
SubPoolsStorage::<Runtime>::insert(1, sub_pools);
|
||||
|
||||
// Adjust the TVL for this non-api usage (direct sub-pool modification)
|
||||
TotalValueLocked::<Runtime>::mutate(|x| *x -= 295);
|
||||
|
||||
// Update the equivalent of the unbonding chunks for the `StakingMock`
|
||||
let mut x = UnbondingBalanceMap::get();
|
||||
*x.get_mut(&default_bonded_account()).unwrap() /= 5;
|
||||
x.get_mut(&default_bonded_account())
|
||||
.unwrap()
|
||||
.get_mut(current_era as usize)
|
||||
.unwrap()
|
||||
.1 /= 2;
|
||||
UnbondingBalanceMap::set(&x);
|
||||
|
||||
Currency::set_balance(
|
||||
&default_bonded_account(),
|
||||
Currency::free_balance(&default_bonded_account()) / 2, // 300
|
||||
);
|
||||
StakingMock::set_bonded_balance(
|
||||
default_bonded_account(),
|
||||
StakingMock::active_stake(&default_bonded_account()).unwrap() / 2,
|
||||
);
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 10);
|
||||
StakingMock::slash_by(1, 5);
|
||||
assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 5);
|
||||
};
|
||||
|
||||
// Advance the current_era to ensure all `with_era` pools will be merged into
|
||||
@@ -3465,6 +3524,7 @@ mod withdraw_unbonded {
|
||||
era: 3
|
||||
},
|
||||
Event::Unbonded { member: 40, pool_id: 1, points: 40, balance: 40, era: 3 },
|
||||
Event::PoolSlashed { pool_id: 1, balance: 5 }
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -3552,7 +3612,7 @@ mod withdraw_unbonded {
|
||||
|
||||
// Given
|
||||
// current bond is 600, we slash it all to 300.
|
||||
StakingMock::set_bonded_balance(default_bonded_account(), 300);
|
||||
StakingMock::slash_by(1, 300);
|
||||
Currency::set_balance(&default_bonded_account(), 300);
|
||||
assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(300));
|
||||
|
||||
@@ -3572,6 +3632,7 @@ mod withdraw_unbonded {
|
||||
Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true },
|
||||
Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true },
|
||||
Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true },
|
||||
Event::PoolSlashed { pool_id: 1, balance: 300 },
|
||||
Event::Unbonded { member: 40, pool_id: 1, balance: 20, points: 20, era: 3 },
|
||||
Event::Unbonded {
|
||||
member: 550,
|
||||
@@ -4051,6 +4112,7 @@ mod withdraw_unbonded {
|
||||
#[test]
|
||||
fn full_multi_step_withdrawing_non_depositor() {
|
||||
ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| {
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 110);
|
||||
// given
|
||||
assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 75));
|
||||
assert_eq!(
|
||||
@@ -4058,6 +4120,9 @@ mod withdraw_unbonded {
|
||||
member_unbonding_eras!(3 => 75)
|
||||
);
|
||||
|
||||
// tvl unchanged.
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 110);
|
||||
|
||||
// progress one era and unbond the leftover.
|
||||
CurrentEra::set(1);
|
||||
assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 25));
|
||||
@@ -4070,6 +4135,8 @@ mod withdraw_unbonded {
|
||||
Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0),
|
||||
Error::<Runtime>::CannotWithdrawAny
|
||||
);
|
||||
// tvl unchanged.
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 110);
|
||||
|
||||
// now the 75 should be free.
|
||||
CurrentEra::set(3);
|
||||
@@ -4089,6 +4156,8 @@ mod withdraw_unbonded {
|
||||
PoolMembers::<Runtime>::get(100).unwrap().unbonding_eras,
|
||||
member_unbonding_eras!(4 => 25)
|
||||
);
|
||||
// tvl updated
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 35);
|
||||
|
||||
// the 25 should be free now, and the member removed.
|
||||
CurrentEra::set(4);
|
||||
@@ -4398,6 +4467,7 @@ mod create {
|
||||
let next_pool_stash = Pools::create_bonded_account(2);
|
||||
let ed = Currency::minimum_balance();
|
||||
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 10);
|
||||
assert!(!BondedPools::<Runtime>::contains_key(2));
|
||||
assert!(!RewardPools::<Runtime>::contains_key(2));
|
||||
assert!(!PoolMembers::<Runtime>::contains_key(11));
|
||||
@@ -4411,6 +4481,7 @@ mod create {
|
||||
456,
|
||||
789
|
||||
));
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 10 + StakingMock::minimum_nominator_bond());
|
||||
|
||||
assert_eq!(Currency::free_balance(&11), 0);
|
||||
assert_eq!(
|
||||
@@ -4701,9 +4772,10 @@ mod set_state {
|
||||
|
||||
// Given
|
||||
unsafe_set_state(1, PoolState::Open);
|
||||
let mut bonded_pool = BondedPool::<Runtime>::get(1).unwrap();
|
||||
bonded_pool.points = 100;
|
||||
bonded_pool.put();
|
||||
// slash the pool to the point that `max_points_to_balance` ratio is
|
||||
// surpassed. Making this pool destroyable by anyone.
|
||||
StakingMock::slash_by(1, 10);
|
||||
|
||||
// When
|
||||
assert_ok!(Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying));
|
||||
// Then
|
||||
@@ -4729,6 +4801,7 @@ mod set_state {
|
||||
pool_events_since_last_call(),
|
||||
vec![
|
||||
Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying },
|
||||
Event::PoolSlashed { pool_id: 1, balance: 0 },
|
||||
Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying },
|
||||
Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }
|
||||
]
|
||||
@@ -4927,8 +5000,10 @@ mod bond_extra {
|
||||
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().points, 10);
|
||||
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().points, 20);
|
||||
assert_eq!(BondedPools::<Runtime>::get(1).unwrap().points, 30);
|
||||
|
||||
assert_eq!(Currency::free_balance(&10), 35);
|
||||
assert_eq!(Currency::free_balance(&20), 20);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 30);
|
||||
|
||||
// when
|
||||
assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::Rewards));
|
||||
@@ -4936,6 +5011,8 @@ mod bond_extra {
|
||||
|
||||
// then
|
||||
assert_eq!(Currency::free_balance(&10), 35);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 31);
|
||||
|
||||
// 10's share of the reward is 1/3, since they gave 10/30 of the total shares.
|
||||
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().points, 10 + 1);
|
||||
assert_eq!(BondedPools::<Runtime>::get(1).unwrap().points, 30 + 1);
|
||||
@@ -4945,6 +5022,8 @@ mod bond_extra {
|
||||
|
||||
// then
|
||||
assert_eq!(Currency::free_balance(&20), 20);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 33);
|
||||
|
||||
// 20's share of the rewards is the other 2/3 of the rewards, since they have 20/30 of
|
||||
// the shares
|
||||
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().points, 20 + 2);
|
||||
@@ -5354,7 +5433,7 @@ mod reward_counter_precision {
|
||||
);
|
||||
|
||||
// slash this pool by 99% of that.
|
||||
StakingMock::set_bonded_balance(default_bonded_account(), DOT + pool_bond / 100);
|
||||
StakingMock::slash_by(1, pool_bond * 99 / 100);
|
||||
|
||||
// some whale now joins with the other half ot the total issuance. This will trigger an
|
||||
// overflow. This test is actually a bit too lenient because all the reward counters are
|
||||
@@ -6868,3 +6947,73 @@ mod commission {
|
||||
})
|
||||
}
|
||||
}
|
||||
mod slash {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn slash_no_subpool_is_tracked() {
|
||||
let bonded = |points, member_counter| BondedPool::<Runtime> {
|
||||
id: 1,
|
||||
inner: BondedPoolInner {
|
||||
commission: Commission::default(),
|
||||
member_counter,
|
||||
points,
|
||||
roles: DEFAULT_ROLES,
|
||||
state: PoolState::Open,
|
||||
},
|
||||
};
|
||||
ExtBuilder::default().with_check(0).build_and_execute(|| {
|
||||
// Given
|
||||
Currency::set_balance(&11, ExistentialDeposit::get() + 2);
|
||||
assert!(!PoolMembers::<Runtime>::contains_key(11));
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 10);
|
||||
|
||||
// When
|
||||
assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1));
|
||||
|
||||
// Then
|
||||
assert_eq!(
|
||||
pool_events_since_last_call(),
|
||||
vec![
|
||||
Event::Created { depositor: 10, pool_id: 1 },
|
||||
Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true },
|
||||
Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true },
|
||||
]
|
||||
);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 12);
|
||||
|
||||
assert_eq!(
|
||||
PoolMembers::<Runtime>::get(11).unwrap(),
|
||||
PoolMember::<Runtime> { pool_id: 1, points: 2, ..Default::default() }
|
||||
);
|
||||
assert_eq!(BondedPool::<Runtime>::get(1).unwrap(), bonded(12, 2));
|
||||
|
||||
// Given
|
||||
// The bonded balance is slashed in half
|
||||
StakingMock::slash_by(1, 6);
|
||||
|
||||
// And
|
||||
Currency::set_balance(&12, ExistentialDeposit::get() + 12);
|
||||
assert!(!PoolMembers::<Runtime>::contains_key(12));
|
||||
|
||||
// When
|
||||
assert_ok!(Pools::join(RuntimeOrigin::signed(12), 12, 1));
|
||||
|
||||
// Then
|
||||
assert_eq!(
|
||||
pool_events_since_last_call(),
|
||||
vec![
|
||||
Event::PoolSlashed { pool_id: 1, balance: 6 },
|
||||
Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true }
|
||||
]
|
||||
);
|
||||
assert_eq!(TotalValueLocked::<T>::get(), 18);
|
||||
|
||||
assert_eq!(
|
||||
PoolMembers::<Runtime>::get(12).unwrap(),
|
||||
PoolMember::<Runtime> { pool_id: 1, points: 24, ..Default::default() }
|
||||
);
|
||||
assert_eq!(BondedPool::<Runtime>::get(1).unwrap(), bonded(12 + 24, 3));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,8 +671,14 @@ impl<T: Config> StakingLedger<T> {
|
||||
// clean unlocking chunks that are set to zero.
|
||||
self.unlocking.retain(|c| !c.value.is_zero());
|
||||
|
||||
T::EventListeners::on_slash(&self.stash, self.active, &slashed_unlocking);
|
||||
pre_slash_total.saturating_sub(self.total)
|
||||
let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
|
||||
T::EventListeners::on_slash(
|
||||
&self.stash,
|
||||
self.active,
|
||||
&slashed_unlocking,
|
||||
final_slashed_amount,
|
||||
);
|
||||
final_slashed_amount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +278,7 @@ impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
|
||||
_pool_account: &AccountId,
|
||||
slashed_bonded: Balance,
|
||||
slashed_chunks: &BTreeMap<EraIndex, Balance>,
|
||||
_total_slashed: Balance,
|
||||
) {
|
||||
LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
|
||||
}
|
||||
|
||||
@@ -121,10 +121,12 @@ pub trait OnStakingUpdate<AccountId, Balance> {
|
||||
/// * `slashed_active` - The new bonded balance of the staker after the slash was applied.
|
||||
/// * `slashed_unlocking` - A map of slashed eras, and the balance of that unlocking chunk after
|
||||
/// the slash is applied. Any era not present in the map is not affected at all.
|
||||
/// * `slashed_total` - The aggregated balance that was lost due to the slash.
|
||||
fn on_slash(
|
||||
_stash: &AccountId,
|
||||
_slashed_active: Balance,
|
||||
_slashed_unlocking: &BTreeMap<EraIndex, Balance>,
|
||||
_slashed_total: Balance,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user