Skip to content

Commit 2b86199

Browse files
wischliweichweich
authored andcommitted
fix: unwanted delegator state removal when tx fails (#266)
* fix: unwanted delegator state removal when tx fails * feat: add v6 migration * feat: add v6 pre check, rm expect * fix: dont panic
1 parent 4049b62 commit 2b86199

File tree

5 files changed

+246
-25
lines changed

5 files changed

+246
-25
lines changed

pallets/parachain-staking/src/lib.rs

+93-19
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ mod types;
172172
use frame_support::pallet;
173173

174174
pub use crate::{default_weights::WeightInfo, pallet::*};
175+
use types::ReplacedDelegator;
175176

176177
#[pallet]
177178
pub mod pallet {
@@ -1391,9 +1392,17 @@ pub mod pallet {
13911392
) -> DispatchResultWithPostInfo {
13921393
let acc = ensure_signed(origin)?;
13931394
let collator = T::Lookup::lookup(collator)?;
1395+
1396+
// check balance
1397+
ensure!(
1398+
pallet_balances::Pallet::<T>::free_balance(acc.clone()) >= amount.into(),
1399+
pallet_balances::Error::<T>::InsufficientBalance
1400+
);
1401+
13941402
// first delegation
13951403
ensure!(<DelegatorState<T>>::get(&acc).is_none(), Error::<T>::AlreadyDelegating);
13961404
ensure!(amount >= T::MinDelegatorStake::get(), Error::<T>::NomStakeBelowMin);
1405+
13971406
// cannot be a collator candidate and delegator with same AccountId
13981407
ensure!(!Self::is_active_candidate(&acc).is_some(), Error::<T>::CandidateExists);
13991408
ensure!(
@@ -1431,12 +1440,15 @@ pub mod pallet {
14311440
.map_err(|_| Error::<T>::MaxCollatorsPerDelegatorExceeded)?;
14321441

14331442
let old_total = state.total;
1434-
// update state and potentially kick a delegator with less staked amount
1435-
state = if num_delegations_pre_insertion == T::MaxDelegatorsPerCollator::get() {
1443+
1444+
// update state and potentially prepare kicking a delegator with less staked
1445+
// amount
1446+
let (state, maybe_kicked_delegator) = if num_delegations_pre_insertion == T::MaxDelegatorsPerCollator::get()
1447+
{
14361448
Self::do_update_delegator(delegation, state)?
14371449
} else {
14381450
state.total = state.total.saturating_add(amount);
1439-
state
1451+
(state, None)
14401452
};
14411453
let new_total = state.total;
14421454

@@ -1446,11 +1458,16 @@ pub mod pallet {
14461458
Self::update_top_candidates(collator.clone(), old_total, new_total);
14471459
}
14481460

1461+
// *** No Fail beyond this point ***
1462+
14491463
// update states
14501464
<CandidatePool<T>>::insert(&collator, state);
14511465
<DelegatorState<T>>::insert(&acc, delegator_state);
14521466
<LastDelegation<T>>::insert(&acc, delegation_counter);
14531467

1468+
// update or clear storage of potentially kicked delegator
1469+
Self::update_kicked_delegator_storage(maybe_kicked_delegator);
1470+
14541471
// update candidates for next round
14551472
let (num_collators, num_delegators, _, _) = Self::update_total_stake();
14561473

@@ -1520,6 +1537,14 @@ pub mod pallet {
15201537
let acc = ensure_signed(origin)?;
15211538
let collator = T::Lookup::lookup(collator)?;
15221539
let mut delegator = <DelegatorState<T>>::get(&acc).ok_or(Error::<T>::NotYetDelegating)?;
1540+
1541+
// check balance
1542+
ensure!(
1543+
pallet_balances::Pallet::<T>::free_balance(acc.clone())
1544+
>= delegator.total.saturating_add(amount).into(),
1545+
pallet_balances::Error::<T>::InsufficientBalance
1546+
);
1547+
15231548
// delegation after first
15241549
ensure!(amount >= T::MinDelegation::get(), Error::<T>::DelegationBelowMin);
15251550
ensure!(
@@ -1535,9 +1560,10 @@ pub mod pallet {
15351560
let num_delegations_pre_insertion: u32 = state.delegators.len().saturated_into();
15361561
ensure!(!state.is_leaving(), Error::<T>::CannotDelegateIfLeaving);
15371562

1538-
// attempt to insert delegator and check for uniqueness
1539-
// NOTE: excess is handled below because we support replacing a delegator with
1540-
// fewer stake
1563+
// attempt to insert delegation, check for uniqueness and update total delegated
1564+
// amount
1565+
// NOTE: excess is handled below because we support replacing a delegator
1566+
// with fewer stake
15411567
ensure!(
15421568
delegator
15431569
.add_delegation(Stake {
@@ -1561,12 +1587,14 @@ pub mod pallet {
15611587

15621588
let old_total = state.total;
15631589

1564-
// update state and potentially kick a delegator with less staked amount
1565-
state = if num_delegations_pre_insertion == T::MaxDelegatorsPerCollator::get() {
1590+
// update state and potentially prepare kicking a delegator with less staked
1591+
// amount
1592+
let (state, maybe_kicked_delegator) = if num_delegations_pre_insertion == T::MaxDelegatorsPerCollator::get()
1593+
{
15661594
Self::do_update_delegator(delegation, state)?
15671595
} else {
15681596
state.total = state.total.saturating_add(amount);
1569-
state
1597+
(state, None)
15701598
};
15711599
let new_total = state.total;
15721600

@@ -1576,11 +1604,16 @@ pub mod pallet {
15761604
Self::update_top_candidates(collator.clone(), old_total, new_total);
15771605
}
15781606

1607+
// *** No Fail beyond this point ***
1608+
15791609
// Update states
15801610
<CandidatePool<T>>::insert(&collator, state);
15811611
<DelegatorState<T>>::insert(&acc, delegator);
15821612
<LastDelegation<T>>::insert(&acc, delegation_counter);
15831613

1614+
// update or clear storage of potentially kicked delegator
1615+
Self::update_kicked_delegator_storage(maybe_kicked_delegator);
1616+
15841617
// update candidates for next round
15851618
let (num_collators, num_delegators, _, _) = Self::update_total_stake();
15861619

@@ -2012,19 +2045,49 @@ pub mod pallet {
20122045
Ok(())
20132046
}
20142047

2015-
fn kick_delegator(delegation: &StakeOf<T>, collator: &T::AccountId) -> DispatchResult {
2048+
/// Check for remaining delegations of the delegator which has been
2049+
/// removed from the given collator.
2050+
///
2051+
/// Returns the removed delegator's address and
2052+
/// * Either the updated delegator state if other delegations are still
2053+
/// remaining
2054+
/// * Or `None`, signalling the delegator state should be cleared once
2055+
/// the transaction cannot fail anymore.
2056+
fn prep_kick_delegator(
2057+
delegation: &StakeOf<T>,
2058+
collator: &T::AccountId,
2059+
) -> Result<ReplacedDelegator<T>, DispatchError> {
20162060
let mut state = <DelegatorState<T>>::get(&delegation.owner).ok_or(Error::<T>::DelegatorNotFound)?;
20172061
state.rm_delegation(collator);
2062+
20182063
// we don't unlock immediately
20192064
Self::prep_unstake(&delegation.owner, delegation.amount, true)?;
20202065

2021-
// clear storage if no delegations are remaining
2066+
// return state if not empty for later removal after all checks have passed
20222067
if state.delegations.is_empty() {
2023-
<DelegatorState<T>>::remove(&delegation.owner);
2068+
Ok(ReplacedDelegator {
2069+
who: delegation.owner.clone(),
2070+
state: None,
2071+
})
20242072
} else {
2025-
<DelegatorState<T>>::insert(&delegation.owner, state);
2073+
Ok(ReplacedDelegator {
2074+
who: delegation.owner.clone(),
2075+
state: Some(state),
2076+
})
2077+
}
2078+
}
2079+
2080+
/// Either clear the storage of a kicked delegator or update its
2081+
/// delegation state if it still contains other delegations.
2082+
fn update_kicked_delegator_storage(delegator: Option<ReplacedDelegator<T>>) {
2083+
match delegator {
2084+
Some(ReplacedDelegator {
2085+
who,
2086+
state: Some(state),
2087+
}) => DelegatorState::<T>::insert(who, state),
2088+
Some(ReplacedDelegator { who, .. }) => DelegatorState::<T>::remove(who),
2089+
_ => (),
20262090
}
2027-
Ok(())
20282091
}
20292092

20302093
/// Select the top `MaxSelectedCandidates` many collators in terms of
@@ -2125,7 +2188,9 @@ pub mod pallet {
21252188
/// amount is at most the minimum staked value of the original delegator
21262189
/// set, an error is returned.
21272190
///
2128-
/// Returns the old delegation that is updated, if any.
2191+
/// Returns a tuple which contains the updated candidate state as well
2192+
/// as the potentially replaced delegation which will be used later when
2193+
/// updating the storage of the replaced delegator.
21292194
///
21302195
/// Emits `DelegationReplaced` if the stake exceeds one of the current
21312196
/// delegations.
@@ -2135,10 +2200,17 @@ pub mod pallet {
21352200
/// bounded by `MaxDelegatorsPerCollator`.
21362201
/// - Reads/Writes: 0
21372202
/// # </weight>
2203+
#[allow(clippy::type_complexity)]
21382204
fn do_update_delegator(
21392205
stake: Stake<T::AccountId, BalanceOf<T>>,
21402206
mut state: Candidate<T::AccountId, BalanceOf<T>, T::MaxDelegatorsPerCollator>,
2141-
) -> Result<CandidateOf<T, T::MaxDelegatorsPerCollator>, DispatchError> {
2207+
) -> Result<
2208+
(
2209+
CandidateOf<T, T::MaxDelegatorsPerCollator>,
2210+
Option<ReplacedDelegator<T>>,
2211+
),
2212+
DispatchError,
2213+
> {
21422214
// attempt to replace the last element of the set
21432215
let stake_to_remove = state
21442216
.delegators
@@ -2159,7 +2231,7 @@ pub mod pallet {
21592231
state.total = state.total.saturating_sub(stake_to_remove.amount);
21602232

21612233
// update storage of kicked delegator
2162-
Self::kick_delegator(&stake_to_remove, &state.id)?;
2234+
let kicked_delegator = Self::prep_kick_delegator(&stake_to_remove, &state.id)?;
21632235

21642236
Self::deposit_event(Event::DelegationReplaced(
21652237
stake.owner,
@@ -2169,9 +2241,11 @@ pub mod pallet {
21692241
state.id.clone(),
21702242
state.total,
21712243
));
2172-
}
21732244

2174-
Ok(state)
2245+
Ok((state, Some(kicked_delegator)))
2246+
} else {
2247+
Ok((state, None))
2248+
}
21752249
}
21762250

21772251
/// Either set or increase the BalanceLock of target account to

pallets/parachain-staking/src/migrations.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ mod v2;
3030
mod v3;
3131
mod v4;
3232
mod v5;
33+
mod v6;
3334

3435
/// A trait that allows version migrators to access the underlying pallet's
3536
/// context, e.g., its Config trait.
@@ -55,6 +56,7 @@ pub enum StakingStorageVersion {
5556
V3_0_0, // Update InflationConfig
5657
V4, // Sort TopCandidates and parachain-stakings by amount
5758
V5, // Remove SelectedCandidates, Count Candidates
59+
V6, // Fix delegator replacement bug
5860
}
5961

6062
#[cfg(feature = "try-runtime")]
@@ -87,7 +89,8 @@ impl<T: Config> VersionMigratorTrait<T> for StakingStorageVersion {
8789
Self::V2_0_0 => v3::pre_migrate::<T>(),
8890
Self::V3_0_0 => v4::pre_migrate::<T>(),
8991
Self::V4 => v5::pre_migrate::<T>(),
90-
Self::V5 => Ok(()),
92+
Self::V5 => v6::pre_migrate::<T>(),
93+
Self::V6 => Ok(()),
9194
}
9295
}
9396

@@ -98,7 +101,8 @@ impl<T: Config> VersionMigratorTrait<T> for StakingStorageVersion {
98101
Self::V2_0_0 => v3::migrate::<T>(),
99102
Self::V3_0_0 => v4::migrate::<T>(),
100103
Self::V4 => v5::migrate::<T>(),
101-
Self::V5 => Weight::zero(),
104+
Self::V5 => v6::migrate::<T>(),
105+
Self::V6 => Weight::zero(),
102106
}
103107
}
104108

@@ -111,7 +115,8 @@ impl<T: Config> VersionMigratorTrait<T> for StakingStorageVersion {
111115
Self::V2_0_0 => v3::post_migrate::<T>(),
112116
Self::V3_0_0 => v4::post_migrate::<T>(),
113117
Self::V4 => v5::post_migrate::<T>(),
114-
Self::V5 => Ok(()),
118+
Self::V5 => v6::post_migrate::<T>(),
119+
Self::V6 => Ok(()),
115120
}
116121
}
117122
}
@@ -133,7 +138,8 @@ impl<T: Config> StakingStorageMigrator<T> {
133138
// Migration happens naturally, no need to point to the latest version
134139
StakingStorageVersion::V3_0_0 => Some(StakingStorageVersion::V4),
135140
StakingStorageVersion::V4 => Some(StakingStorageVersion::V5),
136-
StakingStorageVersion::V5 => None,
141+
StakingStorageVersion::V5 => Some(StakingStorageVersion::V6),
142+
StakingStorageVersion::V6 => None,
137143
}
138144
}
139145

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// KILT Blockchain – https://botlabs.org
2+
// Copyright (C) 2019-2021 BOTLabs GmbH
3+
4+
// The KILT Blockchain is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
9+
// The KILT Blockchain is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
// If you feel like getting in touch with us, you can do so at info@botlabs.org
18+
19+
use frame_support::{dispatch::Weight, traits::Get};
20+
21+
use crate::{
22+
migrations::StakingStorageVersion,
23+
types::{BalanceOf, Delegator},
24+
CandidatePool, Config, DelegatorState, StorageVersion,
25+
};
26+
27+
#[cfg(feature = "try-runtime")]
28+
pub(crate) fn pre_migrate<T: Config>() -> Result<(), &'static str> {
29+
use crate::types::Stake;
30+
31+
assert_eq!(StorageVersion::<T>::get(), StakingStorageVersion::V5);
32+
33+
// ensure each delegator has at most one delegation (ideally should have exactly
34+
// one)
35+
let mut corrupt_delegation: u32 = 0;
36+
for candidate in CandidatePool::<T>::iter_values() {
37+
for delegation in candidate.delegators.into_iter() {
38+
if let Some(state) = DelegatorState::<T>::get(delegation.owner.clone()) {
39+
assert_eq!(state.delegations.len(), 1);
40+
if let Some(Stake::<T::AccountId, BalanceOf<T>> { amount, owner }) =
41+
state.delegations.into_bounded_vec().into_inner().get(0)
42+
{
43+
assert_eq!(amount, &state.total);
44+
assert_eq!(owner, &candidate.id);
45+
}
46+
} else {
47+
corrupt_delegation = corrupt_delegation.saturating_add(1);
48+
}
49+
}
50+
}
51+
52+
log::info!("Found {} corrupt delegations", corrupt_delegation);
53+
Ok(())
54+
}
55+
56+
pub(crate) fn migrate<T: Config>() -> Weight {
57+
log::info!("Migrating staking to StakingStorageVersion::V6");
58+
59+
let mut reads = 0u64;
60+
let mut writes = 1u64;
61+
62+
// iter candidate pool and check whether any delegator has a cleared state
63+
for candidate in CandidatePool::<T>::iter_values() {
64+
reads = reads.saturating_add(1u64);
65+
for delegation in candidate.delegators.into_iter() {
66+
// we do not have to mutate existing entries since MaxCollatorsPerDelegator = 1
67+
if !DelegatorState::<T>::contains_key(delegation.owner.clone()) {
68+
if let Ok(delegator) = Delegator::try_new(candidate.id.clone(), delegation.amount) {
69+
DelegatorState::<T>::insert(delegation.owner, delegator);
70+
writes = writes.saturating_add(1u64);
71+
}
72+
}
73+
reads = reads.saturating_add(1u64);
74+
}
75+
}
76+
77+
// update storage version
78+
StorageVersion::<T>::put(StakingStorageVersion::V6);
79+
log::info!("Completed staking migration to StakingStorageVersion::V6");
80+
81+
T::DbWeight::get().reads_writes(reads, writes)
82+
}
83+
84+
#[cfg(feature = "try-runtime")]
85+
pub(crate) fn post_migrate<T: Config>() -> Result<(), &'static str> {
86+
assert_eq!(StorageVersion::<T>::get(), StakingStorageVersion::V6);
87+
88+
for candidate in CandidatePool::<T>::iter_values() {
89+
for delegation in candidate.delegators.into_iter() {
90+
assert!(DelegatorState::<T>::contains_key(delegation.owner));
91+
}
92+
}
93+
94+
Ok(())
95+
}

0 commit comments

Comments
 (0)