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

Allow nomination pools to chill + fix dismantle scenario #11426

Merged
merged 43 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ea30833
make pool roles optional
kianenigma May 13, 2022
00e8c87
undo lock file changes?
kianenigma May 13, 2022
fa5e95a
add migration
kianenigma May 13, 2022
f2df79e
add the ability for pools to chill themselves
kianenigma May 14, 2022
56cf997
boilerplate of tests
kianenigma May 14, 2022
508bc0a
new system works, tests need fixing
kianenigma May 15, 2022
f83944a
somewhat stable, but I think I found another bug as well
kianenigma May 15, 2022
ac48c37
Fix it all
kianenigma May 16, 2022
cbeb9fb
Master.into()
kianenigma May 16, 2022
031040f
Add more more sophisticated test + capture one more bug.
kianenigma May 16, 2022
efc7b4f
Update frame/staking/src/lib.rs
kianenigma May 16, 2022
60d42f1
reduce the diff a little bit
kianenigma May 17, 2022
02aa7d4
add some test for the slashing bug
kianenigma May 19, 2022
90db26e
cleanup
kianenigma May 19, 2022
5bf6d9c
Merge branch 'kiz-pool-chill' of github.com:paritytech/substrate into…
kianenigma May 19, 2022
1d8c940
Master.into()
kianenigma May 19, 2022
2ec4857
Merge branch 'master' of github.com:paritytech/substrate into kiz-poo…
kianenigma May 20, 2022
a51e408
fix lock file?
kianenigma May 20, 2022
28c8852
Fix
kianenigma May 20, 2022
c9413a2
fmt
kianenigma May 20, 2022
433476d
Merge branch 'master' of github.com:paritytech/substrate into kiz-poo…
kianenigma May 23, 2022
c34b655
Update frame/nomination-pools/src/lib.rs
kianenigma May 23, 2022
d0d75a1
Update frame/nomination-pools/src/lib.rs
kianenigma May 23, 2022
a6afb06
Update frame/nomination-pools/src/lib.rs
kianenigma May 23, 2022
a3a43e7
Update frame/nomination-pools/src/mock.rs
kianenigma May 23, 2022
4b7b0c7
Fix build
kianenigma May 23, 2022
3f66688
Merge branch 'kiz-pool-chill' of github.com:paritytech/substrate into…
kianenigma May 23, 2022
640ec31
fix some fishy tests..
kianenigma May 23, 2022
398ddfe
add one last integrity check for MinCreateBond
kianenigma May 24, 2022
d5dc697
remove bad assertion -- needs to be dealt with later
kianenigma May 24, 2022
05fb517
Merge branch 'master' of github.com:paritytech/substrate into kiz-poo…
kianenigma May 24, 2022
f4dbd0a
nits
shawntabrizi May 25, 2022
0a79c80
Master.into()
kianenigma Jun 8, 2022
78c0310
Merge branch 'kiz-pool-chill' of github.com:paritytech/substrate into…
kianenigma Jun 9, 2022
0fb1125
fix tests and add benchmarks for chill
kianenigma Jun 9, 2022
12773bb
remove stuff
kianenigma Jun 9, 2022
ca475df
fix benchmarks
kianenigma Jun 10, 2022
44a2722
Merge branch 'master' of /~https://github.com/paritytech/substrate into…
Jun 10, 2022
f027faf
cargo run --quiet --profile=production --features=runtime-benchmarks…
Jun 10, 2022
33b581c
Master.into()
kianenigma Jun 11, 2022
c77613f
Merge branch 'kiz-pool-chill' of github.com:paritytech/substrate into…
kianenigma Jun 11, 2022
d69af2c
remove defensive
kianenigma Jun 11, 2022
d318197
Merge branch 'master' of github.com:paritytech/substrate into kiz-poo…
kianenigma Jun 13, 2022
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
1,570 changes: 664 additions & 906 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ members = [
"frame/proxy",
"frame/nomination-pools",
"frame/nomination-pools/benchmarking",
"frame/nomination-pools/test-staking",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a new pallet with end to end tests around staking and pools. I can recommend giving it a read. Other test changes are not really important.

"frame/randomness-collective-flip",
"frame/recovery",
"frame/referenda",
Expand Down
132 changes: 109 additions & 23 deletions frame/nomination-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,16 +452,18 @@ impl<T: Config> PoolMember<T> {
/// Returns `Ok(())` and updates `unbonding_eras` and `points` if success, `Err(_)` otherwise.
fn try_unbond(
&mut self,
points: BalanceOf<T>,
points_dissolved: BalanceOf<T>,
points_issued: BalanceOf<T>,
unbonding_era: EraIndex,
) -> Result<(), Error<T>> {
if let Some(new_points) = self.points.checked_sub(&points) {
if let Some(new_points) = self.points.checked_sub(&points_dissolved) {
match self.unbonding_eras.get_mut(&unbonding_era) {
Some(already_unbonding_points) =>
*already_unbonding_points = already_unbonding_points.saturating_add(points),
*already_unbonding_points =
already_unbonding_points.saturating_add(points_issued),
None => self
.unbonding_eras
.try_insert(unbonding_era, points)
.try_insert(unbonding_era, points_issued)
.map(|old| {
if old.is_some() {
defensive!("value checked to not exist in the map; qed");
Expand Down Expand Up @@ -721,9 +723,13 @@ impl<T: Config> BondedPool<T> {
}

fn is_destroying_and_only_depositor(&self, alleged_depositor_points: BalanceOf<T>) -> bool {
// NOTE: if we add `&& self.member_counter == 1`, then this becomes even more strict and
// ensures that there are no unbonding members hanging around either.
self.is_destroying() && self.points == alleged_depositor_points
// we need to ensure that `self.member_counter == 1` as well, because the depositor's
// initial `MinCreateBond` (or more) is what guarantees that the ledger of the pool does not
// get killed in the staking system, and that it does not fall below `MinimumNominatorBond`,
// which could prevent other non-depositor members from fully leaving. Thus, all members
// must withdraw, then depositor can unbond, and finally withdraw after waiting another
// cycle.
self.is_destroying() && self.points == alleged_depositor_points && self.member_counter == 1
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
}

/// Whether or not the pool is ok to be in `PoolSate::Open`. If this returns an `Err`, then the
Expand Down Expand Up @@ -979,9 +985,13 @@ impl<T: Config> UnbondPool<T> {
}

/// Issue points and update the balance given `new_balance`.
fn issue(&mut self, new_funds: BalanceOf<T>) {
self.points = self.points.saturating_add(self.balance_to_point(new_funds));
///
/// Returns the actual amounts of points issued.
fn issue(&mut self, new_funds: BalanceOf<T>) -> BalanceOf<T> {
let new_points = self.balance_to_point(new_funds);
self.points = self.points.saturating_add(new_points);
self.balance = self.balance.saturating_add(new_funds);
new_points
}

/// Dissolve some points from the unbonding pool, reducing the balance of the pool
Expand Down Expand Up @@ -1136,6 +1146,15 @@ pub mod pallet {
///
/// This is the amount that the depositor must put as their initial stake in the pool, as an
/// indication of "skin in the game".
///
/// This is the value that will always exist in the staking ledger of the pool bonded account
/// while all other accounts leave.
///
/// A sane configuration should thus ensure that this value is large enough such that it is more
/// than `MinimumNominatorBond` and `ExistentialDeposit`. The pool's bonded account's stash
/// falling behind `MinimumNominatorBond` it cannot call unbond, and the pool bonded account's
/// stash falling below `ExistentialDeposit` means the ledger can be killed prematurely in a
/// call to `T::StakingInterface::withdraw_unbonded`.
#[pallet::storage]
pub type MinCreateBond<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;

Expand Down Expand Up @@ -1242,9 +1261,33 @@ pub mod pallet {
/// A payout has been made to a member.
PaidOut { member: T::AccountId, pool_id: PoolId, payout: BalanceOf<T> },
/// A member has unbonded from their pool.
Unbonded { member: T::AccountId, pool_id: PoolId, amount: BalanceOf<T> },
///
/// - `balance` is the corresponding balance of the number of points that have been
/// requested to be unbonded (the argument of the `unbond` transaction) from the bonded
/// pool.
/// - `points` is the number of points that are issued as a result of
/// `balance` into the corresponding unbonding pool.
///
/// In the absence of slashing, these values will match. In the presence of slashing, the
/// number of points that are issued in the unbonding pool will be less than the amount
/// requested to be unbonded.
Unbonded {
member: T::AccountId,
pool_id: PoolId,
balance: BalanceOf<T>,
points: BalanceOf<T>,
},
/// A member has withdrawn from their pool.
Withdrawn { member: T::AccountId, pool_id: PoolId, amount: BalanceOf<T> },
///
/// The given number of `points` have been dissolved in return of `balance`.
///
/// Similar to `Unbonded` event, in the absence of slashing, these values will match.
Withdrawn {
member: T::AccountId,
pool_id: PoolId,
balance: BalanceOf<T>,
points: BalanceOf<T>,
},
/// A pool has been destroyed.
Destroyed { pool_id: PoolId },
/// The state of a pool has changed
Expand All @@ -1260,6 +1303,10 @@ pub mod pallet {
state_toggler: Option<T::AccountId>,
nominator: Option<T::AccountId>,
},
/// The active balance of pool `pool_id` has been slashed to `balance`.
PoolSlashed { pool_id: PoolId, balance: BalanceOf<T> },
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved
/// The unbond pool at `era` of pool `pool_id` has been slashed to `balance`.
UnbondingPoolSlashed { pool_id: PoolId, era: EraIndex, balance: BalanceOf<T> },
}

#[pallet::error]
Expand All @@ -1276,8 +1323,6 @@ pub mod pallet {
/// An account is already delegating in another pool. An account may only belong to one
/// pool at a time.
AccountBelongsToOtherPool,
/// The pool has insufficient balance to bond as a nominator.
InsufficientBond,
/// The member is already unbonding in this era.
AlreadyUnbonding,
/// The member is fully unbonded (and thus cannot access the bonded and reward pool
Expand Down Expand Up @@ -1500,9 +1545,6 @@ pub mod pallet {
let current_era = T::StakingInterface::current_era();
let unbond_era = T::StakingInterface::bonding_duration().saturating_add(current_era);

// Try and unbond in the member map.
member.try_unbond(unbonding_points, unbond_era)?;
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved

// Unbond in the actual underlying nominator.
let unbonding_balance = bonded_pool.dissolve(unbonding_points);
T::StakingInterface::unbond(bonded_pool.bonded_account(), unbonding_balance)?;
Expand All @@ -1523,17 +1565,21 @@ pub mod pallet {
.defensive_map_err(|_| Error::<T>::DefensiveError)?;
}

sub_pools
let actual_unbonding_points = sub_pools
.with_era
.get_mut(&unbond_era)
// The above check ensures the pool exists.
.defensive_ok_or_else(|| Error::<T>::DefensiveError)?
.issue(unbonding_balance);

// Try and unbond in the member map.
member.try_unbond(unbonding_points, actual_unbonding_points, unbond_era)?;

Self::deposit_event(Event::<T>::Unbonded {
member: member_account.clone(),
pool_id: member.pool_id,
amount: unbonding_balance,
points: actual_unbonding_points,
balance: unbonding_balance,
});

// Now that we know everything has worked write the items to storage.
Expand Down Expand Up @@ -1616,14 +1662,16 @@ pub mod pallet {

// Before calculate the `balance_to_unbond`, with call withdraw unbonded to ensure the
// `transferrable_balance` is correct.
T::StakingInterface::withdraw_unbonded(
let destroyed = T::StakingInterface::withdraw_unbonded(
bonded_pool.bonded_account(),
num_slashing_spans,
)?;

let mut sum_unlocked_points: BalanceOf<T> = Zero::zero();
let balance_to_unbond = withdrawn_points
.iter()
.fold(BalanceOf::<T>::zero(), |accumulator, (era, unlocked_points)| {
sum_unlocked_points = sum_unlocked_points.saturating_add(*unlocked_points);
if let Some(era_pool) = sub_pools.with_era.get_mut(&era) {
let balance_to_unbond = era_pool.dissolve(*unlocked_points);
if era_pool.points.is_zero() {
Expand Down Expand Up @@ -1656,7 +1704,8 @@ pub mod pallet {
Self::deposit_event(Event::<T>::Withdrawn {
member: member_account.clone(),
pool_id: member.pool_id,
amount: balance_to_unbond,
points: sum_unlocked_points,
balance: balance_to_unbond,
});

let post_info_weight = if member.total_points().is_zero() {
Expand Down Expand Up @@ -1784,6 +1833,13 @@ pub mod pallet {
Ok(())
}

/// Nominate on behalf of the pool.
///
/// The dispatch origin of this call must be signed by the pool nominator or the the pool
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
/// root role.
///
/// This directly forward the call to the staking pallet, on behalf of the pool bonded
/// account.
#[pallet::weight(T::WeightInfo::nominate(validators.len() as u32))]
pub fn nominate(
origin: OriginFor<T>,
Expand All @@ -1793,10 +1849,28 @@ pub mod pallet {
let who = ensure_signed(origin)?;
let bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_nominate(&who), Error::<T>::NotNominator);
T::StakingInterface::nominate(bonded_pool.bonded_account(), validators)?;
Ok(())
T::StakingInterface::nominate(bonded_pool.bonded_account(), validators)
}

/// Nominate on behalf of the pool.
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
///
/// The dispatch origin of this call must be signed by the pool nominator or the the pool
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
/// root role, same as [`Pallet::nominate`].
///
/// This directly forward the call to the staking pallet, on behalf of the pool bonded
/// account.
#[pallet::weight(T::WeightInfo::chill())]
pub fn chill(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved
let who = ensure_signed(origin)?;
let bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_nominate(&who), Error::<T>::NotNominator);
T::StakingInterface::chill(bonded_pool.bonded_account())
}

/// Set a new state for the pool.
///
/// The dispatch origin of this call must be signed by the state toggler, or the root role
/// of the pool.
#[pallet::weight(T::WeightInfo::set_state())]
pub fn set_state(
origin: OriginFor<T>,
Expand All @@ -1823,6 +1897,10 @@ pub mod pallet {
Ok(())
}

/// Set a new metadata for the pool.
///
/// The dispatch origin of this call must be signed by the state toggler, or the root role
/// of the pool.
#[pallet::weight(T::WeightInfo::set_metadata(metadata.len() as u32))]
pub fn set_metadata(
origin: OriginFor<T>,
Expand Down Expand Up @@ -2237,6 +2315,7 @@ impl<T: Config> Pallet<T> {
if level.is_zero() {
return Ok(())
}

// note: while a bit wacky, since they have the same key, even collecting to vec should
// result in the same set of keys, in the same order.
let bonded_pools = BondedPools::<T>::iter_keys().collect::<Vec<_>>();
Expand Down Expand Up @@ -2337,9 +2416,16 @@ impl<T: Config> OnStakerSlash<T::AccountId, BalanceOf<T>> for Pallet<T> {
};
for (era, slashed_balance) in slashed_unlocking.iter() {
if let Some(pool) = sub_pools.with_era.get_mut(era) {
pool.balance = *slashed_balance
pool.balance = *slashed_balance;
Self::deposit_event(Event::<T>::UnbondingPoolSlashed {
era: *era,
pool_id,
balance: *slashed_balance,
});
}
}

Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: _slashed_bonded });
shawntabrizi marked this conversation as resolved.
Show resolved Hide resolved
SubPoolsStorage::<T>::insert(pool_id, sub_pools);
}
}
Expand Down
57 changes: 41 additions & 16 deletions frame/nomination-pools/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use super::*;
use crate::{self as pools};
use frame_support::{assert_ok, parameter_types, PalletId};
use frame_system::RawOrigin;
use std::collections::HashMap;

pub type AccountId = u128;
pub type Balance = u128;
Expand All @@ -20,17 +19,19 @@ pub fn default_reward_account() -> AccountId {
parameter_types! {
pub static CurrentEra: EraIndex = 0;
pub static BondingDuration: EraIndex = 3;
static BondedBalanceMap: HashMap<AccountId, Balance> = Default::default();
static UnbondingBalanceMap: HashMap<AccountId, Balance> = Default::default();
pub storage BondedBalanceMap: BTreeMap<AccountId, Balance> = Default::default();
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
pub storage UnbondingBalanceMap: BTreeMap<AccountId, Balance> = Default::default();
#[derive(Clone, PartialEq)]
pub static MaxUnbonding: u32 = 8;
pub static Nominations: Vec<AccountId> = vec![];
pub storage Nominations: Vec<AccountId> = vec![];
}

pub struct StakingMock;
impl StakingMock {
pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) {
BONDED_BALANCE_MAP.with(|m| m.borrow_mut().insert(who, bonded));
let mut x = BondedBalanceMap::get();
x.insert(who, bonded);
BondedBalanceMap::set(&x)
}
}

Expand Down Expand Up @@ -66,22 +67,33 @@ impl sp_staking::StakingInterface for StakingMock {
}

fn bond_extra(who: Self::AccountId, extra: Self::Balance) -> DispatchResult {
BONDED_BALANCE_MAP.with(|m| *m.borrow_mut().get_mut(&who).unwrap() += extra);
let mut x = BondedBalanceMap::get();
x.get_mut(&who).map(|v| *v += extra);
BondedBalanceMap::set(&x);
Ok(())
}

fn unbond(who: Self::AccountId, amount: Self::Balance) -> DispatchResult {
BONDED_BALANCE_MAP.with(|m| *m.borrow_mut().get_mut(&who).unwrap() -= amount);
UNBONDING_BALANCE_MAP
.with(|m| *m.borrow_mut().entry(who).or_insert(Self::Balance::zero()) += amount);
let mut x = BondedBalanceMap::get();
*x.get_mut(&who).unwrap() = x.get_mut(&who).unwrap().saturating_sub(amount);
BondedBalanceMap::set(&x);
let mut y = UnbondingBalanceMap::get();
*y.entry(who).or_insert(Self::Balance::zero()) += amount;
UnbondingBalanceMap::set(&y);
Ok(())
}

fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result<u64, DispatchError> {
fn chill(_: Self::AccountId) -> sp_runtime::DispatchResult {
Ok(())
}

fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result<bool, DispatchError> {
// Simulates removing unlocking chunks and only having the bonded balance locked
let _maybe_new_free = UNBONDING_BALANCE_MAP.with(|m| m.borrow_mut().remove(&who));
let mut x = UnbondingBalanceMap::get();
x.remove(&who);
UnbondingBalanceMap::set(&x);

Ok(100)
Ok(UnbondingBalanceMap::get().is_empty())
}

fn bond(
Expand All @@ -95,7 +107,7 @@ impl sp_staking::StakingInterface for StakingMock {
}

fn nominate(_: Self::AccountId, nominations: Vec<Self::AccountId>) -> DispatchResult {
Nominations::set(nominations);
Nominations::set(&nominations);
Ok(())
}
}
Expand Down Expand Up @@ -263,7 +275,8 @@ pub(crate) fn unsafe_set_state(pool_id: PoolId, state: PoolState) -> Result<(),
}

parameter_types! {
static ObservedEvents: usize = 0;
static PoolsEvents: usize = 0;
static BalancesEvents: usize = 0;
}

/// All events of this pallet.
Expand All @@ -273,8 +286,20 @@ pub(crate) fn pool_events_since_last_call() -> Vec<super::Event<Runtime>> {
.map(|r| r.event)
.filter_map(|e| if let Event::Pools(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>();
let already_seen = ObservedEvents::get();
ObservedEvents::set(events.len());
let already_seen = PoolsEvents::get();
PoolsEvents::set(events.len());
events.into_iter().skip(already_seen).collect()
}

/// All events of this pallet.
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) fn balances_events_since_last_call() -> Vec<pallet_balances::Event<Runtime>> {
let events = System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let Event::Balances(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>();
let already_seen = BalancesEvents::get();
BalancesEvents::set(events.len());
events.into_iter().skip(already_seen).collect()
}

Expand Down
Loading