Skip to content

Commit 2e36f57

Browse files
authored
Allow pool to be destroyed with an extra (erroneous) consumer reference on the pool account (#4503)
addresses #4440 (will close once we have this in prod runtimes). related: #2037. An extra consumer reference is preventing pools to be destroyed. When a pool is ready to be destroyed, we can safely clear the consumer references if any. Notably, I only check for one extra consumer reference since that is a known bug. Anything more indicates possibly another issue and we probably don't want to silently absorb those errors as well. After this change, pools with extra consumer reference should be able to destroy normally.
1 parent 65c5248 commit 2e36f57

File tree

5 files changed

+206
-2
lines changed

5 files changed

+206
-2
lines changed

prdoc/pr_4503.prdoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
2+
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
3+
4+
title: Patch pool to handle extra consumer ref when destroying.
5+
6+
doc:
7+
- audience: Runtime User
8+
description: |
9+
An erroneous consumer reference on the pool account is preventing pools from being destroyed. This patch removes the extra reference if it exists when the pool account is destroyed.
10+
11+
crates:
12+
- name: pallet-nomination-pools
13+
bump: patch

substrate/frame/nomination-pools/src/lib.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,7 @@ pub mod pallet {
22542254
SubPoolsStorage::<T>::get(member.pool_id).ok_or(Error::<T>::SubPoolsNotFound)?;
22552255

22562256
bonded_pool.ok_to_withdraw_unbonded_with(&caller, &member_account)?;
2257+
let pool_account = bonded_pool.bonded_account();
22572258

22582259
// NOTE: must do this after we have done the `ok_to_withdraw_unbonded_other_with` check.
22592260
let withdrawn_points = member.withdraw_unlocked(current_era);
@@ -2262,7 +2263,7 @@ pub mod pallet {
22622263
// Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the
22632264
// `transferrable_balance` is correct.
22642265
let stash_killed =
2265-
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?;
2266+
T::Staking::withdraw_unbonded(pool_account.clone(), num_slashing_spans)?;
22662267

22672268
// defensive-only: the depositor puts enough funds into the stash so that it will only
22682269
// be destroyed when they are leaving.
@@ -2271,6 +2272,20 @@ pub mod pallet {
22712272
Error::<T>::Defensive(DefensiveError::BondedStashKilledPrematurely)
22722273
);
22732274

2275+
if stash_killed {
2276+
// Maybe an extra consumer left on the pool account, if so, remove it.
2277+
if frame_system::Pallet::<T>::consumers(&pool_account) == 1 {
2278+
frame_system::Pallet::<T>::dec_consumers(&pool_account);
2279+
}
2280+
2281+
// Note: This is not pretty, but we have to do this because of a bug where old pool
2282+
// accounts might have had an extra consumer increment. We know at this point no
2283+
// other pallet should depend on pool account so safe to do this.
2284+
// Refer to following issues:
2285+
// - https://github.com/paritytech/polkadot-sdk/issues/4440
2286+
// - https://github.com/paritytech/polkadot-sdk/issues/2037
2287+
}
2288+
22742289
let mut sum_unlocked_points: BalanceOf<T> = Zero::zero();
22752290
let balance_to_unbond = withdrawn_points
22762291
.iter()

substrate/frame/nomination-pools/src/mock.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ impl sp_staking::StakingInterface for StakingMock {
160160
Pools::on_withdraw(&who, unlocking_before.saturating_sub(unlocking(&staker_map)));
161161

162162
UnbondingBalanceMap::set(&unbonding_map);
163-
Ok(UnbondingBalanceMap::get().is_empty() && BondedBalanceMap::get().is_empty())
163+
Ok(UnbondingBalanceMap::get().get(&who).unwrap().is_empty() &&
164+
BondedBalanceMap::get().get(&who).unwrap().is_zero())
164165
}
165166

166167
fn bond(stash: &Self::AccountId, value: Self::Balance, _: &Self::AccountId) -> DispatchResult {

substrate/frame/nomination-pools/src/tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4594,6 +4594,92 @@ mod withdraw_unbonded {
45944594
assert_eq!(ClaimPermissions::<Runtime>::contains_key(20), false);
45954595
});
45964596
}
4597+
4598+
#[test]
4599+
fn destroy_works_without_erroneous_extra_consumer() {
4600+
ExtBuilder::default().ed(1).build_and_execute(|| {
4601+
// 10 is the depositor for pool 1, with min join bond 10.
4602+
// set pool to destroying.
4603+
unsafe_set_state(1, PoolState::Destroying);
4604+
4605+
// set current era
4606+
CurrentEra::set(1);
4607+
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10));
4608+
4609+
assert_eq!(
4610+
pool_events_since_last_call(),
4611+
vec![
4612+
Event::Created { depositor: 10, pool_id: 1 },
4613+
Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true },
4614+
Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 },
4615+
]
4616+
);
4617+
4618+
// move to era when unbonded funds can be withdrawn.
4619+
CurrentEra::set(4);
4620+
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0));
4621+
4622+
assert_eq!(
4623+
pool_events_since_last_call(),
4624+
vec![
4625+
Event::Withdrawn { member: 10, pool_id: 1, points: 10, balance: 10 },
4626+
Event::MemberRemoved { pool_id: 1, member: 10 },
4627+
Event::Destroyed { pool_id: 1 },
4628+
]
4629+
);
4630+
4631+
// pool is destroyed.
4632+
assert!(!Metadata::<T>::contains_key(1));
4633+
// ensure the pool account is reaped.
4634+
assert!(!frame_system::Account::<T>::contains_key(&Pools::create_bonded_account(1)));
4635+
})
4636+
}
4637+
4638+
#[test]
4639+
fn destroy_works_with_erroneous_extra_consumer() {
4640+
ExtBuilder::default().ed(1).build_and_execute(|| {
4641+
// 10 is the depositor for pool 1, with min join bond 10.
4642+
let pool_one = Pools::create_bonded_account(1);
4643+
4644+
// set pool to destroying.
4645+
unsafe_set_state(1, PoolState::Destroying);
4646+
4647+
// set current era
4648+
CurrentEra::set(1);
4649+
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10));
4650+
4651+
assert_eq!(
4652+
pool_events_since_last_call(),
4653+
vec![
4654+
Event::Created { depositor: 10, pool_id: 1 },
4655+
Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true },
4656+
Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 },
4657+
]
4658+
);
4659+
4660+
// move to era when unbonded funds can be withdrawn.
4661+
CurrentEra::set(4);
4662+
4663+
// increment consumer by 1 reproducing the erroneous consumer bug.
4664+
// refer https://github.com/paritytech/polkadot-sdk/issues/4440.
4665+
assert_ok!(frame_system::Pallet::<T>::inc_consumers(&pool_one));
4666+
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0));
4667+
4668+
assert_eq!(
4669+
pool_events_since_last_call(),
4670+
vec![
4671+
Event::Withdrawn { member: 10, pool_id: 1, points: 10, balance: 10 },
4672+
Event::MemberRemoved { pool_id: 1, member: 10 },
4673+
Event::Destroyed { pool_id: 1 },
4674+
]
4675+
);
4676+
4677+
// pool is destroyed.
4678+
assert!(!Metadata::<T>::contains_key(1));
4679+
// ensure the pool account is reaped.
4680+
assert!(!frame_system::Account::<T>::contains_key(&pool_one));
4681+
})
4682+
}
45974683
}
45984684

45994685
mod create {

substrate/frame/nomination-pools/test-staking/src/lib.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,95 @@ fn pool_lifecycle_e2e() {
193193
})
194194
}
195195

196+
#[test]
197+
fn destroy_pool_with_erroneous_consumer() {
198+
new_test_ext().execute_with(|| {
199+
// create the pool, we know this has id 1.
200+
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 50, 10, 10, 10));
201+
assert_eq!(LastPoolId::<Runtime>::get(), 1);
202+
203+
// expect consumers on pool account to be 2 (staking lock and an explicit inc by staking).
204+
assert_eq!(frame_system::Pallet::<T>::consumers(&POOL1_BONDED), 2);
205+
206+
// increment consumer by 1 reproducing the erroneous consumer bug.
207+
// refer https://github.com/paritytech/polkadot-sdk/issues/4440.
208+
assert_ok!(frame_system::Pallet::<T>::inc_consumers(&POOL1_BONDED));
209+
assert_eq!(frame_system::Pallet::<T>::consumers(&POOL1_BONDED), 3);
210+
211+
// have the pool nominate.
212+
assert_ok!(Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3]));
213+
214+
assert_eq!(
215+
staking_events_since_last_call(),
216+
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 50 }]
217+
);
218+
assert_eq!(
219+
pool_events_since_last_call(),
220+
vec![
221+
PoolsEvent::Created { depositor: 10, pool_id: 1 },
222+
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 50, joined: true },
223+
]
224+
);
225+
226+
// pool goes into destroying
227+
assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying));
228+
229+
assert_eq!(
230+
pool_events_since_last_call(),
231+
vec![PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying },]
232+
);
233+
234+
// move to era 1
235+
CurrentEra::<Runtime>::set(Some(1));
236+
237+
// depositor need to chill before unbonding
238+
assert_noop!(
239+
Pools::unbond(RuntimeOrigin::signed(10), 10, 50),
240+
pallet_staking::Error::<Runtime>::InsufficientBond
241+
);
242+
243+
assert_ok!(Pools::chill(RuntimeOrigin::signed(10), 1));
244+
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 50));
245+
246+
assert_eq!(
247+
staking_events_since_last_call(),
248+
vec![
249+
StakingEvent::Chilled { stash: POOL1_BONDED },
250+
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 50 },
251+
]
252+
);
253+
assert_eq!(
254+
pool_events_since_last_call(),
255+
vec![PoolsEvent::Unbonded {
256+
member: 10,
257+
pool_id: 1,
258+
points: 50,
259+
balance: 50,
260+
era: 1 + 3
261+
}]
262+
);
263+
264+
// waiting bonding duration:
265+
CurrentEra::<Runtime>::set(Some(1 + 3));
266+
// this should work even with an extra consumer count on pool account.
267+
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 1));
268+
269+
// pools is fully destroyed now.
270+
assert_eq!(
271+
staking_events_since_last_call(),
272+
vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 50 },]
273+
);
274+
assert_eq!(
275+
pool_events_since_last_call(),
276+
vec![
277+
PoolsEvent::Withdrawn { member: 10, pool_id: 1, points: 50, balance: 50 },
278+
PoolsEvent::MemberRemoved { pool_id: 1, member: 10 },
279+
PoolsEvent::Destroyed { pool_id: 1 }
280+
]
281+
);
282+
})
283+
}
284+
196285
#[test]
197286
fn pool_chill_e2e() {
198287
new_test_ext().execute_with(|| {

0 commit comments

Comments
 (0)