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

Fix off by one error in proportional slashing #11782

Merged
merged 15 commits into from
Jul 13, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
267 changes: 265 additions & 2 deletions frame/nomination-pools/test-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ mod mock;
use frame_support::{assert_noop, assert_ok, bounded_btree_map, traits::Currency};
use mock::*;
use pallet_nomination_pools::{
Error as PoolsError, Event as PoolsEvent, LastPoolId, PoolMember, PoolMembers, PoolState,
BondedPools, Error as PoolsError, Event as PoolsEvent, LastPoolId, PoolMember, PoolMembers,
PoolState,
};
use pallet_staking::{CurrentEra, Event as StakingEvent, Payee, RewardDestination};

Expand Down Expand Up @@ -273,7 +274,7 @@ fn pool_slash_e2e() {
30,
&mut Default::default(),
&mut Default::default(),
1, // slash era 1, affects chunks at era 5 onwards.
2, // slash era 2, affects chunks at era 5 onwards.
);

assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Slashed(POOL1_BONDED, 30)]);
Expand Down Expand Up @@ -371,3 +372,265 @@ fn pool_slash_e2e() {
);
});
}

#[test]
fn pool_slash_proportional() {
// a typical example where 3 pool members unbond in era 99, 100, and 101, and a slash that
// happened in era 100 should only affect the latter two.
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
BondingDuration::set(28);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);

// create the pool, we know this has id 1.
assert_ok!(Pools::create(Origin::signed(10), 40, 10, 10, 10));
assert_eq!(LastPoolId::<T>::get(), 1);

assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Bonded(POOL1_BONDED, 40)]);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);

// have two members join
let bond = 20;
assert_ok!(Pools::join(Origin::signed(20), bond, 1));
assert_ok!(Pools::join(Origin::signed(21), bond, 1));
assert_ok!(Pools::join(Origin::signed(22), bond, 1));

assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded(POOL1_BONDED, bond),
StakingEvent::Bonded(POOL1_BONDED, bond),
StakingEvent::Bonded(POOL1_BONDED, bond),
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true },
PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: bond, joined: true },
PoolsEvent::Bonded { member: 22, pool_id: 1, bonded: bond, joined: true },
]
);

// now let's progress a lot.
CurrentEra::<T>::set(Some(99));

// and unbond
assert_ok!(Pools::unbond(Origin::signed(20), 20, bond));

assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded(POOL1_BONDED, bond),]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: bond, points: bond }]
);

CurrentEra::<T>::set(Some(100));
assert_ok!(Pools::unbond(Origin::signed(21), 21, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded(POOL1_BONDED, bond),]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: bond, points: bond }]
);

CurrentEra::<T>::set(Some(101));
assert_ok!(Pools::unbond(Origin::signed(22), 22, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded(POOL1_BONDED, bond),]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 22, pool_id: 1, balance: bond, points: bond }]
);

// Apply a slash that happened in era 100. This is typically applied with a delay.
// Of the total 100, 50 is slashed.
assert_eq!(BondedPools::<T>::get(1).unwrap().points, 40);
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
50,
&mut Default::default(),
&mut Default::default(),
100,
);

assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Slashed(POOL1_BONDED, 50)]);
assert_eq!(
pool_events_since_last_call(),
vec![
// This last pool got slashed only the leftover dust. Otherwise in principle, this
// chunk/pool should have not been affected.
// TODO: We might improve this in a future PR
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 127, balance: 19 },
// This pool got slashed 12.5, which rounded down to 12.
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 128, balance: 8 },
// This pool got slashed 12.5, which rounded down to 12.
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 129, balance: 8 },
// Bonded pool got slashed for 25, remaining 15 in it.
PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 }
]
);
});
}

#[test]
fn pool_slash_non_proportional_only_bonded_pool() {
// A typical example where a pool member unbonds in era 99, and he can get away with a slash tha
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
// happened in era 99, as long as the pool has enough active bond to cover the slash. If
// everything else in the slashing/staking system works, this should always be the case.
// Nonetheless, `ledger.slash` has been written such that it will slash greedily from any chunk
// if it runs out of chunks that it thinks should be affected by the slash.
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
BondingDuration::set(28);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);

// create the pool, we know this has id 1.
assert_ok!(Pools::create(Origin::signed(10), 40, 10, 10, 10));
assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Bonded(POOL1_BONDED, 40)]);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);

// have two members join
let bond = 20;
assert_ok!(Pools::join(Origin::signed(20), bond, 1));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded(POOL1_BONDED, bond)
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }
]
);

// progress and unbond.
CurrentEra::<T>::set(Some(99));
assert_ok!(Pools::unbond(Origin::signed(20), 20, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded(POOL1_BONDED, bond)]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: bond, points: bond }]
);

// slash for 30. This will be deducted only from the bonded pool.
CurrentEra::<T>::set(Some(100));
assert_eq!(BondedPools::<T>::get(1).unwrap().points, 40);
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
30,
&mut Default::default(),
&mut Default::default(),
100,
);

assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Slashed(POOL1_BONDED, 30)]);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::PoolSlashed { pool_id: 1, balance: 10 }
]
);
});
}

#[test]
fn pool_slash_non_proportional_bonded_pool_and_chunks() {
// An uncommon example where even though some funds are unlocked such that they should not be
// affected by a slash, we still slash out of them. This should not happen at all. If a
// nomination has unbonded, from the next era onwards, their exposure will drop, so if an era
// happens in that era, then their share of that slash should naturally be less, such that only
// their active ledger stake is enough to compensate it.
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
BondingDuration::set(28);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);

// create the pool, we know this has id 1.
assert_ok!(Pools::create(Origin::signed(10), 40, 10, 10, 10));
assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Bonded(POOL1_BONDED, 40)]);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);

// have two members join
let bond = 20;
assert_ok!(Pools::join(Origin::signed(20), bond, 1));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded(POOL1_BONDED, bond)
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }
]
);

// progress and unbond.
CurrentEra::<T>::set(Some(99));
assert_ok!(Pools::unbond(Origin::signed(20), 20, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded(POOL1_BONDED, bond)]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: bond, points: bond }]
);

// slash 50. This will be deducted only from the bonded pool and one of the unbonding pools.
CurrentEra::<T>::set(Some(100));
assert_eq!(BondedPools::<T>::get(1).unwrap().points, 40);
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
50,
&mut Default::default(),
&mut Default::default(),
100,
);

assert_eq!(staking_events_since_last_call(), vec![StakingEvent::Slashed(POOL1_BONDED, 50)]);
assert_eq!(
pool_events_since_last_call(),
vec![
// out of 20, 10 was taken.
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 127, balance: 10 },
// out of 40, all was taken.
PoolsEvent::PoolSlashed { pool_id: 1, balance: 0 }
]
);
});
}
7 changes: 5 additions & 2 deletions frame/nomination-pools/test-staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type AccountIndex = u32;
type BlockNumber = u64;
type Balance = u128;

pub(crate) type T = Runtime;

pub(crate) const POOL1_BONDED: AccountId = 20318131474730217858575332831085u128;
pub(crate) const POOL1_REWARD: AccountId = 20397359637244482196168876781421u128;

Expand Down Expand Up @@ -194,13 +196,14 @@ frame_support::construct_runtime!(
);

pub fn new_test_ext() -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let mut storage = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
let _ = pallet_nomination_pools::GenesisConfig::<Runtime> {
min_join_bond: 2,
min_create_bond: 2,
max_pools: Some(3),
max_members_per_pool: Some(3),
max_members: Some(3 * 3),
max_members_per_pool: Some(5),
max_members: Some(3 * 5),
}
.assimilate_storage(&mut storage)
.unwrap();
Expand Down
Loading