mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 15:51:12 +00:00
[NPoS] Fix for Reward Deficit in the pool (#1255)
closes https://github.com/paritytech/polkadot-sdk/issues/158. partially addresses https://github.com/paritytech/polkadot-sdk/issues/226. Instead of fragile calculation of current balance by looking at `free balance - ED`, Nomination Pool now freezes ED in the pool reward account to restrict an account from going below minimum balance. This also has a nice side effect that if ED changes, we know how much is the imbalance in ED frozen in the pool and the current required ED. A pool operator can diligently top up the pool with the deficit in ED or vice versa, withdraw the excess they transferred to the pool. ## Notable changes - New call `adjust_pool_deposit`: Allows to top up the deficit or withdraw the excess deposited funds to the pool. - Uses Fungible trait (instead of Currency trait). Since NP was not doing any locking/reserving previously, no migration is needed for this. - One time migration of freezing ED from each of the existing pools (not very PoV friendly but fine for relay chain).
This commit is contained in:
@@ -353,12 +353,16 @@
|
||||
|
||||
use codec::Codec;
|
||||
use frame_support::{
|
||||
defensive, ensure,
|
||||
defensive, defensive_assert, ensure,
|
||||
pallet_prelude::{MaxEncodedLen, *},
|
||||
storage::bounded_btree_map::BoundedBTreeMap,
|
||||
traits::{
|
||||
Currency, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating,
|
||||
ExistenceRequirement, Get,
|
||||
fungible::{
|
||||
Inspect as FunInspect, InspectFreeze, Mutate as FunMutate,
|
||||
MutateFreeze as FunMutateFreeze,
|
||||
},
|
||||
tokens::{Fortitude, Preservation},
|
||||
Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get,
|
||||
},
|
||||
DefaultNoBound, PalletError,
|
||||
};
|
||||
@@ -380,7 +384,6 @@ use sp_runtime::TryRuntimeError;
|
||||
|
||||
/// The log target of this pallet.
|
||||
pub const LOG_TARGET: &str = "runtime::nomination-pools";
|
||||
|
||||
// syntactic sugar for logging.
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
@@ -405,7 +408,7 @@ pub use weights::WeightInfo;
|
||||
|
||||
/// The balance type used by the currency system.
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
<<T as Config>::Currency as FunInspect<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
/// Type used for unique identifier of each pool.
|
||||
pub type PoolId = u32;
|
||||
|
||||
@@ -1005,10 +1008,15 @@ impl<T: Config> BondedPool<T> {
|
||||
self
|
||||
}
|
||||
|
||||
/// The pools balance that is transferrable.
|
||||
fn transferrable_balance(&self) -> BalanceOf<T> {
|
||||
/// The pools balance that is transferable provided it is expendable by staking pallet.
|
||||
fn transferable_balance(&self) -> BalanceOf<T> {
|
||||
let account = self.bonded_account();
|
||||
T::Currency::free_balance(&account)
|
||||
// Note on why we can't use `Currency::reducible_balance`: Since pooled account has a
|
||||
// provider (staking pallet), the account can not be set expendable by
|
||||
// `pallet-nomination-pool`. This means reducible balance always returns balance preserving
|
||||
// ED in the account. What we want though is transferable balance given the account can be
|
||||
// dusted.
|
||||
T::Currency::balance(&account)
|
||||
.saturating_sub(T::Staking::active_stake(&account).unwrap_or_default())
|
||||
}
|
||||
|
||||
@@ -1201,8 +1209,8 @@ impl<T: Config> BondedPool<T> {
|
||||
&bonded_account,
|
||||
amount,
|
||||
match ty {
|
||||
BondType::Create => ExistenceRequirement::AllowDeath,
|
||||
BondType::Later => ExistenceRequirement::KeepAlive,
|
||||
BondType::Create => Preservation::Expendable,
|
||||
BondType::Later => Preservation::Preserve,
|
||||
},
|
||||
)?;
|
||||
// We must calculate the points issued *before* we bond who's funds, else points:balance
|
||||
@@ -1300,13 +1308,22 @@ impl<T: Config> RewardPool<T> {
|
||||
self.total_commission_pending =
|
||||
self.total_commission_pending.saturating_add(new_pending_commission);
|
||||
|
||||
// Store the total payouts at the time of this update. Total payouts are essentially the
|
||||
// entire historical balance of the reward pool, equating to the current balance + the total
|
||||
// rewards that have left the pool + the total commission that has left the pool.
|
||||
self.last_recorded_total_payouts = balance
|
||||
// Total payouts are essentially the entire historical balance of the reward pool, equating
|
||||
// to the current balance + the total rewards that have left the pool + the total commission
|
||||
// that has left the pool.
|
||||
let last_recorded_total_payouts = balance
|
||||
.checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed))
|
||||
.ok_or(Error::<T>::OverflowRisk)?;
|
||||
|
||||
// Store the total payouts at the time of this update.
|
||||
//
|
||||
// An increase in ED could cause `last_recorded_total_payouts` to decrease but we should not
|
||||
// allow that to happen since an already paid out reward cannot decrease. The reward account
|
||||
// might go in deficit temporarily in this exceptional case but it will be corrected once
|
||||
// new rewards are added to the pool.
|
||||
self.last_recorded_total_payouts =
|
||||
self.last_recorded_total_payouts.max(last_recorded_total_payouts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1380,8 +1397,11 @@ impl<T: Config> RewardPool<T> {
|
||||
///
|
||||
/// This is sum of all the rewards that are claimable by pool members.
|
||||
fn current_balance(id: PoolId) -> BalanceOf<T> {
|
||||
T::Currency::free_balance(&Pallet::<T>::create_reward_account(id))
|
||||
.saturating_sub(T::Currency::minimum_balance())
|
||||
T::Currency::reducible_balance(
|
||||
&Pallet::<T>::create_reward_account(id),
|
||||
Preservation::Expendable,
|
||||
Fortitude::Polite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1487,6 +1507,7 @@ impl<T: Config> SubPools<T> {
|
||||
/// `no_era` pool. This is guaranteed to at least be equal to the staking `UnbondingDuration`. For
|
||||
/// improved UX [`Config::PostUnbondingPoolsWindow`] should be configured to a non-zero value.
|
||||
pub struct TotalUnbondingPools<T: Config>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> Get<u32> for TotalUnbondingPools<T> {
|
||||
fn get() -> u32 {
|
||||
// NOTE: this may be dangerous in the scenario bonding_duration gets decreased because
|
||||
@@ -1504,7 +1525,7 @@ pub mod pallet {
|
||||
use sp_runtime::Perbill;
|
||||
|
||||
/// The current storage version.
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(5);
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
@@ -1518,8 +1539,12 @@ pub mod pallet {
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: weights::WeightInfo;
|
||||
|
||||
/// The nominating balance.
|
||||
type Currency: Currency<Self::AccountId>;
|
||||
/// The currency type used for nomination pool.
|
||||
type Currency: FunMutate<Self::AccountId>
|
||||
+ FunMutateFreeze<Self::AccountId, Id = Self::RuntimeFreezeReason>;
|
||||
|
||||
/// The overarching freeze reason.
|
||||
type RuntimeFreezeReason: From<FreezeReason>;
|
||||
|
||||
/// The type that is used for reward counter.
|
||||
///
|
||||
@@ -1685,6 +1710,7 @@ pub mod pallet {
|
||||
fn build(&self) {
|
||||
MinJoinBond::<T>::put(self.min_join_bond);
|
||||
MinCreateBond::<T>::put(self.min_create_bond);
|
||||
|
||||
if let Some(max_pools) = self.max_pools {
|
||||
MaxPools::<T>::put(max_pools);
|
||||
}
|
||||
@@ -1770,6 +1796,10 @@ pub mod pallet {
|
||||
},
|
||||
/// Pool commission has been claimed.
|
||||
PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf<T> },
|
||||
/// Topped up deficit in frozen ED of the reward pool.
|
||||
MinBalanceDeficitAdjusted { pool_id: PoolId, amount: BalanceOf<T> },
|
||||
/// Claimed excess frozen ED of af the reward pool.
|
||||
MinBalanceExcessAdjusted { pool_id: PoolId, amount: BalanceOf<T> },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
@@ -1845,6 +1875,8 @@ pub mod pallet {
|
||||
InvalidPoolId,
|
||||
/// Bonding extra is restricted to the exact pending reward amount.
|
||||
BondExtraRestricted,
|
||||
/// No imbalance in the ED deposit for the pool.
|
||||
NothingToAdjust,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, PartialEq, TypeInfo, PalletError, RuntimeDebug)]
|
||||
@@ -1868,6 +1900,14 @@ pub mod pallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// A reason for freezing funds.
|
||||
#[pallet::composite_enum]
|
||||
pub enum FreezeReason {
|
||||
/// Pool reward account is restricted from going below Existential Deposit.
|
||||
#[codec(index = 0)]
|
||||
PoolMinBalance,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Stake funds with a pool. The amount to bond is transferred from the member to the
|
||||
@@ -2140,7 +2180,7 @@ pub mod pallet {
|
||||
ensure!(!withdrawn_points.is_empty(), Error::<T>::CannotWithdrawAny);
|
||||
|
||||
// Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the
|
||||
// `transferrable_balance` is correct.
|
||||
// `transferable_balance` is correct.
|
||||
let stash_killed =
|
||||
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?;
|
||||
|
||||
@@ -2175,13 +2215,13 @@ pub mod pallet {
|
||||
// don't exist. This check is also defensive in cases where the unbond pool does not
|
||||
// update its balance (e.g. a bug in the slashing hook.) We gracefully proceed in
|
||||
// order to ensure members can leave the pool and it can be destroyed.
|
||||
.min(bonded_pool.transferrable_balance());
|
||||
.min(bonded_pool.transferable_balance());
|
||||
|
||||
T::Currency::transfer(
|
||||
&bonded_pool.bonded_account(),
|
||||
&member_account,
|
||||
balance_to_unbond,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
Preservation::Expendable,
|
||||
)
|
||||
.defensive()?;
|
||||
|
||||
@@ -2237,7 +2277,7 @@ pub mod pallet {
|
||||
/// # Note
|
||||
///
|
||||
/// In addition to `amount`, the caller will transfer the existential deposit; so the caller
|
||||
/// needs at have at least `amount + existential_deposit` transferrable.
|
||||
/// needs at have at least `amount + existential_deposit` transferable.
|
||||
#[pallet::call_index(6)]
|
||||
#[pallet::weight(T::WeightInfo::create())]
|
||||
pub fn create(
|
||||
@@ -2631,6 +2671,20 @@ pub mod pallet {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_claim_commission(who, pool_id)
|
||||
}
|
||||
|
||||
/// Top up the deficit or withdraw the excess ED from the pool.
|
||||
///
|
||||
/// When a pool is created, the pool depositor transfers ED to the reward account of the
|
||||
/// pool. ED is subject to change and over time, the deposit in the reward account may be
|
||||
/// insufficient to cover the ED deficit of the pool or vice-versa where there is excess
|
||||
/// deposit to the pool. This call allows anyone to adjust the ED deposit of the
|
||||
/// pool by either topping up the deficit or claiming the excess.
|
||||
#[pallet::call_index(21)]
|
||||
#[pallet::weight(T::WeightInfo::adjust_pool_deposit())]
|
||||
pub fn adjust_pool_deposit(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_adjust_pool_deposit(who, pool_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
@@ -2681,6 +2735,9 @@ impl<T: Config> Pallet<T> {
|
||||
RewardPools::<T>::remove(bonded_pool.id);
|
||||
SubPoolsStorage::<T>::remove(bonded_pool.id);
|
||||
|
||||
// remove the ED restriction from the pool reward account.
|
||||
let _ = Self::unfreeze_pool_deposit(&bonded_pool.reward_account()).defensive();
|
||||
|
||||
// Kill accounts from storage by making their balance go below ED. We assume that the
|
||||
// accounts have no references that would prevent destruction once we get to this point. We
|
||||
// don't work with the system pallet directly, but
|
||||
@@ -2688,26 +2745,44 @@ impl<T: Config> Pallet<T> {
|
||||
// consumers anyway.
|
||||
// 2. the bonded account should become a 'killed stash' in the staking system, and all of
|
||||
// its consumers removed.
|
||||
debug_assert_eq!(frame_system::Pallet::<T>::consumers(&reward_account), 0);
|
||||
debug_assert_eq!(frame_system::Pallet::<T>::consumers(&bonded_account), 0);
|
||||
debug_assert_eq!(
|
||||
T::Staking::total_stake(&bonded_account).unwrap_or_default(),
|
||||
Zero::zero()
|
||||
defensive_assert!(
|
||||
frame_system::Pallet::<T>::consumers(&reward_account) == 0,
|
||||
"reward account of dissolving pool should have no consumers"
|
||||
);
|
||||
defensive_assert!(
|
||||
frame_system::Pallet::<T>::consumers(&bonded_account) == 0,
|
||||
"bonded account of dissolving pool should have no consumers"
|
||||
);
|
||||
defensive_assert!(
|
||||
T::Staking::total_stake(&bonded_account).unwrap_or_default() == Zero::zero(),
|
||||
"dissolving pool should not have any stake in the staking pallet"
|
||||
);
|
||||
|
||||
// This shouldn't fail, but if it does we don't really care. Remaining balance can consist
|
||||
// of unclaimed pending commission, errorneous transfers to the reward account, etc.
|
||||
let reward_pool_remaining = T::Currency::free_balance(&reward_account);
|
||||
// of unclaimed pending commission, erroneous transfers to the reward account, etc.
|
||||
let reward_pool_remaining = T::Currency::reducible_balance(
|
||||
&reward_account,
|
||||
Preservation::Expendable,
|
||||
Fortitude::Polite,
|
||||
);
|
||||
let _ = T::Currency::transfer(
|
||||
&reward_account,
|
||||
&bonded_pool.roles.depositor,
|
||||
reward_pool_remaining,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
Preservation::Expendable,
|
||||
);
|
||||
|
||||
// NOTE: this is purely defensive.
|
||||
T::Currency::make_free_balance_be(&reward_account, Zero::zero());
|
||||
T::Currency::make_free_balance_be(&bonded_pool.bonded_account(), Zero::zero());
|
||||
defensive_assert!(
|
||||
T::Currency::total_balance(&reward_account) == Zero::zero(),
|
||||
"could not transfer all amount to depositor while dissolving pool"
|
||||
);
|
||||
defensive_assert!(
|
||||
T::Currency::total_balance(&bonded_pool.bonded_account()) == Zero::zero(),
|
||||
"dissolving pool should not have any balance"
|
||||
);
|
||||
// NOTE: Defensively force set balance to zero.
|
||||
T::Currency::set_balance(&reward_account, Zero::zero());
|
||||
T::Currency::set_balance(&bonded_pool.bonded_account(), Zero::zero());
|
||||
|
||||
Self::deposit_event(Event::<T>::Destroyed { pool_id: bonded_pool.id });
|
||||
// Remove bonded pool metadata.
|
||||
@@ -2838,7 +2913,7 @@ impl<T: Config> Pallet<T> {
|
||||
pending_rewards,
|
||||
// defensive: the depositor has put existential deposit into the pool and it stays
|
||||
// untouched, reward account shall not die.
|
||||
ExistenceRequirement::KeepAlive,
|
||||
Preservation::Preserve,
|
||||
)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::PaidOut {
|
||||
@@ -2846,7 +2921,6 @@ impl<T: Config> Pallet<T> {
|
||||
pool_id: member.pool_id,
|
||||
payout: pending_rewards,
|
||||
});
|
||||
|
||||
Ok(pending_rewards)
|
||||
}
|
||||
|
||||
@@ -2881,13 +2955,17 @@ impl<T: Config> Pallet<T> {
|
||||
bonded_pool.try_inc_members()?;
|
||||
let points = bonded_pool.try_bond_funds(&who, amount, BondType::Create)?;
|
||||
|
||||
// Transfer the minimum balance for the reward account.
|
||||
T::Currency::transfer(
|
||||
&who,
|
||||
&bonded_pool.reward_account(),
|
||||
T::Currency::minimum_balance(),
|
||||
ExistenceRequirement::AllowDeath,
|
||||
Preservation::Expendable,
|
||||
)?;
|
||||
|
||||
// Restrict reward account balance from going below ED.
|
||||
Self::freeze_pool_deposit(&bonded_pool.reward_account())?;
|
||||
|
||||
PoolMembers::<T>::insert(
|
||||
who.clone(),
|
||||
PoolMember::<T> {
|
||||
@@ -2999,7 +3077,7 @@ impl<T: Config> Pallet<T> {
|
||||
&bonded_pool.reward_account(),
|
||||
&payee,
|
||||
commission,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
Preservation::Preserve,
|
||||
)?;
|
||||
|
||||
// Add pending commission to total claimed counter.
|
||||
@@ -3007,7 +3085,6 @@ impl<T: Config> Pallet<T> {
|
||||
reward_pool.total_commission_claimed.saturating_add(commission);
|
||||
// Reset total pending commission counter to zero.
|
||||
reward_pool.total_commission_pending = Zero::zero();
|
||||
// Commit reward pool updates
|
||||
RewardPools::<T>::insert(pool_id, reward_pool);
|
||||
|
||||
Self::deposit_event(Event::<T>::PoolCommissionClaimed { pool_id, commission });
|
||||
@@ -3029,6 +3106,55 @@ impl<T: Config> Pallet<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_adjust_pool_deposit(who: T::AccountId, pool: PoolId) -> DispatchResult {
|
||||
let bonded_pool = BondedPool::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
|
||||
let reward_acc = &bonded_pool.reward_account();
|
||||
let pre_frozen_balance =
|
||||
T::Currency::balance_frozen(&FreezeReason::PoolMinBalance.into(), reward_acc);
|
||||
let min_balance = T::Currency::minimum_balance();
|
||||
|
||||
if pre_frozen_balance == min_balance {
|
||||
return Err(Error::<T>::NothingToAdjust.into())
|
||||
}
|
||||
|
||||
// Update frozen amount with current ED.
|
||||
Self::freeze_pool_deposit(reward_acc)?;
|
||||
|
||||
if pre_frozen_balance > min_balance {
|
||||
// Transfer excess back to depositor.
|
||||
let excess = pre_frozen_balance.saturating_sub(min_balance);
|
||||
T::Currency::transfer(reward_acc, &who, excess, Preservation::Preserve)?;
|
||||
Self::deposit_event(Event::<T>::MinBalanceExcessAdjusted {
|
||||
pool_id: pool,
|
||||
amount: excess,
|
||||
});
|
||||
} else {
|
||||
// Transfer ED deficit from depositor to the pool
|
||||
let deficit = min_balance.saturating_sub(pre_frozen_balance);
|
||||
T::Currency::transfer(&who, reward_acc, deficit, Preservation::Expendable)?;
|
||||
Self::deposit_event(Event::<T>::MinBalanceDeficitAdjusted {
|
||||
pool_id: pool,
|
||||
amount: deficit,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply freeze on reward account to restrict it from going below ED.
|
||||
pub(crate) fn freeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult {
|
||||
T::Currency::set_freeze(
|
||||
&FreezeReason::PoolMinBalance.into(),
|
||||
reward_acc,
|
||||
T::Currency::minimum_balance(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Removes the ED freeze on the reward account of `pool_id`.
|
||||
pub fn unfreeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult {
|
||||
T::Currency::thaw(&FreezeReason::PoolMinBalance.into(), reward_acc)
|
||||
}
|
||||
|
||||
/// Ensure the correctness of the state of this pallet.
|
||||
///
|
||||
/// This should be valid before or after each state transition of this pallet.
|
||||
@@ -3094,14 +3220,20 @@ impl<T: Config> Pallet<T> {
|
||||
|
||||
for id in reward_pools {
|
||||
let account = Self::create_reward_account(id);
|
||||
if T::Currency::free_balance(&account) < T::Currency::minimum_balance() {
|
||||
if T::Currency::reducible_balance(&account, Preservation::Expendable, Fortitude::Polite) <
|
||||
T::Currency::minimum_balance()
|
||||
{
|
||||
log!(
|
||||
warn,
|
||||
"reward pool of {:?}: {:?} (ed = {:?}), should only happen because ED has \
|
||||
changed recently. Pool operators should be notified to top up the reward \
|
||||
account",
|
||||
id,
|
||||
T::Currency::free_balance(&account),
|
||||
T::Currency::reducible_balance(
|
||||
&account,
|
||||
Preservation::Expendable,
|
||||
Fortitude::Polite
|
||||
),
|
||||
T::Currency::minimum_balance(),
|
||||
)
|
||||
}
|
||||
@@ -3135,9 +3267,8 @@ impl<T: Config> Pallet<T> {
|
||||
let pending_rewards_lt_leftover_bal = RewardPool::<T>::current_balance(id) >=
|
||||
pools_members_pending_rewards.get(&id).copied().unwrap_or_default();
|
||||
|
||||
// this is currently broken in Kusama, a fix is being worked on in
|
||||
// <https://github.com/paritytech/polkadot-sdk/pull/1255>. until it is fixed, log a
|
||||
// warning instead of panicing with an `ensure` statement.
|
||||
// If this happens, this is most likely due to an old bug and not a recent code change.
|
||||
// We warn about this in try-runtime checks but do not panic.
|
||||
if !pending_rewards_lt_leftover_bal {
|
||||
log::warn!(
|
||||
"pool {:?}, sum pending rewards = {:?}, remaining balance = {:?}",
|
||||
@@ -3199,9 +3330,39 @@ impl<T: Config> Pallet<T> {
|
||||
);
|
||||
}
|
||||
|
||||
// Warn if any pool has incorrect ED frozen. We don't want to fail hard as this could be a
|
||||
// result of an intentional ED change.
|
||||
let _ = Self::check_ed_imbalance()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if any pool have an incorrect amount of ED frozen.
|
||||
///
|
||||
/// This can happen if the ED has changed since the pool was created.
|
||||
#[cfg(any(feature = "try-runtime", feature = "runtime-benchmarks", test, debug_assertions))]
|
||||
pub fn check_ed_imbalance() -> Result<(), DispatchError> {
|
||||
let mut failed: u32 = 0;
|
||||
BondedPools::<T>::iter_keys().for_each(|id| {
|
||||
let reward_acc = Self::create_reward_account(id);
|
||||
let frozen_balance =
|
||||
T::Currency::balance_frozen(&FreezeReason::PoolMinBalance.into(), &reward_acc);
|
||||
|
||||
let expected_frozen_balance = T::Currency::minimum_balance();
|
||||
if frozen_balance != expected_frozen_balance {
|
||||
failed += 1;
|
||||
log::warn!(
|
||||
"pool {:?} has incorrect ED frozen that can result from change in ED. Expected = {:?}, Actual = {:?}",
|
||||
id,
|
||||
expected_frozen_balance,
|
||||
frozen_balance,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ensure!(failed == 0, "Some pools do not have correct ED frozen");
|
||||
Ok(())
|
||||
}
|
||||
/// Fully unbond the shares of `member`, when executed from `origin`.
|
||||
///
|
||||
/// This is useful for backwards compatibility with the majority of tests that only deal with
|
||||
|
||||
@@ -23,55 +23,95 @@ use sp_std::{collections::btree_map::BTreeMap, vec::Vec};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use sp_runtime::TryRuntimeError;
|
||||
|
||||
pub mod v1 {
|
||||
/// Exports for versioned migration `type`s for this pallet.
|
||||
pub mod versioned_migrations {
|
||||
use super::*;
|
||||
|
||||
/// Wrapper over `MigrateToV6` with convenience version checks.
|
||||
pub type V5toV6<T> = frame_support::migrations::VersionedMigration<
|
||||
5,
|
||||
6,
|
||||
v6::MigrateToV6<T>,
|
||||
crate::pallet::Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v6 {
|
||||
use super::*;
|
||||
|
||||
/// This migration would restrict reward account of pools to go below ED by doing a named
|
||||
/// freeze on all the existing pools.
|
||||
pub struct MigrateToV6<T>(sp_std::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Config> MigrateToV6<T> {
|
||||
fn freeze_ed(pool_id: PoolId) -> Result<(), ()> {
|
||||
let reward_acc = Pallet::<T>::create_reward_account(pool_id);
|
||||
Pallet::<T>::freeze_pool_deposit(&reward_acc).map_err(|e| {
|
||||
log!(error, "Failed to freeze ED for pool {} with error: {:?}", pool_id, e);
|
||||
()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV6<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut success = 0u64;
|
||||
let mut fail = 0u64;
|
||||
|
||||
BondedPools::<T>::iter_keys().for_each(|p| {
|
||||
if Self::freeze_ed(p).is_ok() {
|
||||
success.saturating_inc();
|
||||
} else {
|
||||
fail.saturating_inc();
|
||||
}
|
||||
});
|
||||
|
||||
if fail > 0 {
|
||||
log!(error, "Failed to freeze ED for {} pools", fail);
|
||||
} else {
|
||||
log!(info, "Freezing ED succeeded for {} pools", success);
|
||||
}
|
||||
|
||||
let total = success.saturating_add(fail);
|
||||
// freeze_ed = r:2 w:2
|
||||
// reads: (freeze_ed + bonded pool key) * total
|
||||
// writes: freeze_ed * total
|
||||
T::DbWeight::get().reads_writes(3u64.saturating_mul(total), 2u64.saturating_mul(total))
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
// there should be no ED imbalances anymore..
|
||||
Pallet::<T>::check_ed_imbalance()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod v5 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Decode)]
|
||||
pub struct OldPoolRoles<AccountId> {
|
||||
pub depositor: AccountId,
|
||||
pub root: AccountId,
|
||||
pub nominator: AccountId,
|
||||
pub bouncer: AccountId,
|
||||
pub struct OldRewardPool<T: Config> {
|
||||
last_recorded_reward_counter: T::RewardCounter,
|
||||
last_recorded_total_payouts: BalanceOf<T>,
|
||||
total_rewards_claimed: BalanceOf<T>,
|
||||
}
|
||||
|
||||
impl<AccountId> OldPoolRoles<AccountId> {
|
||||
fn migrate_to_v1(self) -> PoolRoles<AccountId> {
|
||||
PoolRoles {
|
||||
depositor: self.depositor,
|
||||
root: Some(self.root),
|
||||
nominator: Some(self.nominator),
|
||||
bouncer: Some(self.bouncer),
|
||||
impl<T: Config> OldRewardPool<T> {
|
||||
fn migrate_to_v5(self) -> RewardPool<T> {
|
||||
RewardPool {
|
||||
last_recorded_reward_counter: self.last_recorded_reward_counter,
|
||||
last_recorded_total_payouts: self.last_recorded_total_payouts,
|
||||
total_rewards_claimed: self.total_rewards_claimed,
|
||||
total_commission_pending: Zero::zero(),
|
||||
total_commission_claimed: Zero::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Decode)]
|
||||
pub struct OldBondedPoolInner<T: Config> {
|
||||
pub points: BalanceOf<T>,
|
||||
pub state: PoolState,
|
||||
pub member_counter: u32,
|
||||
pub roles: OldPoolRoles<T::AccountId>,
|
||||
}
|
||||
|
||||
impl<T: Config> OldBondedPoolInner<T> {
|
||||
fn migrate_to_v1(self) -> BondedPoolInner<T> {
|
||||
// Note: `commission` field not introduced to `BondedPoolInner` until
|
||||
// migration 4.
|
||||
BondedPoolInner {
|
||||
points: self.points,
|
||||
commission: Commission::default(),
|
||||
member_counter: self.member_counter,
|
||||
state: self.state,
|
||||
roles: self.roles.migrate_to_v1(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trivial migration which makes the roles of each pool optional.
|
||||
///
|
||||
/// Note: The depositor is not optional since they can never change.
|
||||
pub struct MigrateToV1<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
/// This migration adds `total_commission_pending` and `total_commission_claimed` field to every
|
||||
/// `RewardPool`, if any.
|
||||
pub struct MigrateToV5<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV5<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::current_storage_version();
|
||||
let onchain = Pallet::<T>::on_chain_storage_version();
|
||||
@@ -83,33 +123,284 @@ pub mod v1 {
|
||||
onchain
|
||||
);
|
||||
|
||||
if current == 1 && onchain == 0 {
|
||||
// this is safe to execute on any runtime that has a bounded number of pools.
|
||||
if current == 5 && onchain == 4 {
|
||||
let mut translated = 0u64;
|
||||
BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
|
||||
RewardPools::<T>::translate::<OldRewardPool<T>, _>(|_id, old_value| {
|
||||
translated.saturating_inc();
|
||||
Some(old_value.migrate_to_v1())
|
||||
Some(old_value.migrate_to_v5())
|
||||
});
|
||||
|
||||
current.put::<Pallet<T>>();
|
||||
|
||||
log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
|
||||
|
||||
// reads: translated + onchain version.
|
||||
// writes: translated + current.put.
|
||||
T::DbWeight::get().reads_writes(translated + 1, translated + 1)
|
||||
} else {
|
||||
log!(info, "Migration did not executed. This probably should be removed");
|
||||
log!(info, "Migration did not execute. This probably should be removed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
// new version must be set.
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let rpool_keys = RewardPools::<T>::iter_keys().count();
|
||||
let rpool_values = RewardPools::<T>::iter_values().count();
|
||||
if rpool_keys != rpool_values {
|
||||
log!(info, "🔥 There are {} undecodable RewardPools in storage. This migration will try to correct them. keys: {}, values: {}", rpool_keys.saturating_sub(rpool_values), rpool_keys, rpool_values);
|
||||
}
|
||||
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() == 1,
|
||||
"The onchain version must be updated after the migration."
|
||||
PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
|
||||
"There are undecodable PoolMembers in storage. This migration will not fix that."
|
||||
);
|
||||
ensure!(
|
||||
BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
|
||||
"There are undecodable BondedPools in storage. This migration will not fix that."
|
||||
);
|
||||
ensure!(
|
||||
SubPoolsStorage::<T>::iter_keys().count() ==
|
||||
SubPoolsStorage::<T>::iter_values().count(),
|
||||
"There are undecodable SubPools in storage. This migration will not fix that."
|
||||
);
|
||||
ensure!(
|
||||
Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
|
||||
"There are undecodable Metadata in storage. This migration will not fix that."
|
||||
);
|
||||
|
||||
Ok((rpool_values as u64).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let old_rpool_values: u64 = Decode::decode(&mut &data[..]).unwrap();
|
||||
let rpool_keys = RewardPools::<T>::iter_keys().count() as u64;
|
||||
let rpool_values = RewardPools::<T>::iter_values().count() as u64;
|
||||
ensure!(
|
||||
rpool_keys == rpool_values,
|
||||
"There are STILL undecodable RewardPools - migration failed"
|
||||
);
|
||||
|
||||
if old_rpool_values != rpool_values {
|
||||
log!(
|
||||
info,
|
||||
"🎉 Fixed {} undecodable RewardPools.",
|
||||
rpool_values.saturating_sub(old_rpool_values)
|
||||
);
|
||||
}
|
||||
|
||||
// ensure all RewardPools items now contain `total_commission_pending` and
|
||||
// `total_commission_claimed` field.
|
||||
ensure!(
|
||||
RewardPools::<T>::iter().all(|(_, reward_pool)| reward_pool
|
||||
.total_commission_pending >=
|
||||
Zero::zero() && reward_pool
|
||||
.total_commission_claimed >=
|
||||
Zero::zero()),
|
||||
"a commission value has been incorrectly set"
|
||||
);
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 5,
|
||||
"nomination-pools::migration::v5: wrong storage version"
|
||||
);
|
||||
|
||||
// These should not have been touched - just in case.
|
||||
ensure!(
|
||||
PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
|
||||
"There are undecodable PoolMembers in storage."
|
||||
);
|
||||
ensure!(
|
||||
BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
|
||||
"There are undecodable BondedPools in storage."
|
||||
);
|
||||
ensure!(
|
||||
SubPoolsStorage::<T>::iter_keys().count() ==
|
||||
SubPoolsStorage::<T>::iter_values().count(),
|
||||
"There are undecodable SubPools in storage."
|
||||
);
|
||||
ensure!(
|
||||
Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
|
||||
"There are undecodable Metadata in storage."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v4 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Decode)]
|
||||
pub struct OldBondedPoolInner<T: Config> {
|
||||
pub points: BalanceOf<T>,
|
||||
pub state: PoolState,
|
||||
pub member_counter: u32,
|
||||
pub roles: PoolRoles<T::AccountId>,
|
||||
}
|
||||
|
||||
impl<T: Config> OldBondedPoolInner<T> {
|
||||
fn migrate_to_v4(self) -> BondedPoolInner<T> {
|
||||
BondedPoolInner {
|
||||
commission: Commission::default(),
|
||||
member_counter: self.member_counter,
|
||||
points: self.points,
|
||||
state: self.state,
|
||||
roles: self.roles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates from `v3` directly to `v5` to avoid the broken `v4` migration.
|
||||
#[allow(deprecated)]
|
||||
pub type MigrateV3ToV5<T, U> = (v4::MigrateToV4<T, U>, v5::MigrateToV5<T>);
|
||||
|
||||
/// # Warning
|
||||
///
|
||||
/// To avoid mangled storage please use `MigrateV3ToV5` instead.
|
||||
/// See: github.com/paritytech/substrate/pull/13715
|
||||
///
|
||||
/// This migration adds a `commission` field to every `BondedPoolInner`, if
|
||||
/// any.
|
||||
#[deprecated(
|
||||
note = "To avoid mangled storage please use `MigrateV3ToV5` instead. See: github.com/paritytech/substrate/pull/13715"
|
||||
)]
|
||||
pub struct MigrateToV4<T, U>(sp_std::marker::PhantomData<(T, U)>);
|
||||
#[allow(deprecated)]
|
||||
impl<T: Config, U: Get<Perbill>> OnRuntimeUpgrade for MigrateToV4<T, U> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::current_storage_version();
|
||||
let onchain = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log!(
|
||||
info,
|
||||
"Running migration with current storage version {:?} / onchain {:?}",
|
||||
current,
|
||||
onchain
|
||||
);
|
||||
|
||||
if onchain == 3 {
|
||||
log!(warn, "Please run MigrateToV5 immediately after this migration. See github.com/paritytech/substrate/pull/13715");
|
||||
let initial_global_max_commission = U::get();
|
||||
GlobalMaxCommission::<T>::set(Some(initial_global_max_commission));
|
||||
log!(
|
||||
info,
|
||||
"Set initial global max commission to {:?}.",
|
||||
initial_global_max_commission
|
||||
);
|
||||
|
||||
let mut translated = 0u64;
|
||||
BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
|
||||
translated.saturating_inc();
|
||||
Some(old_value.migrate_to_v4())
|
||||
});
|
||||
|
||||
StorageVersion::new(4).put::<Pallet<T>>();
|
||||
log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
|
||||
|
||||
// reads: translated + onchain version.
|
||||
// writes: translated + current.put + initial global commission.
|
||||
T::DbWeight::get().reads_writes(translated + 1, translated + 2)
|
||||
} else {
|
||||
log!(info, "Migration did not execute. This probably should be removed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
// ensure all BondedPools items now contain an `inner.commission: Commission` field.
|
||||
ensure!(
|
||||
BondedPools::<T>::iter().all(|(_, inner)|
|
||||
// Check current
|
||||
(inner.commission.current.is_none() ||
|
||||
inner.commission.current.is_some()) &&
|
||||
// Check max
|
||||
(inner.commission.max.is_none() || inner.commission.max.is_some()) &&
|
||||
// Check change_rate
|
||||
(inner.commission.change_rate.is_none() ||
|
||||
inner.commission.change_rate.is_some()) &&
|
||||
// Check throttle_from
|
||||
(inner.commission.throttle_from.is_none() ||
|
||||
inner.commission.throttle_from.is_some())),
|
||||
"a commission value has not been set correctly"
|
||||
);
|
||||
ensure!(
|
||||
GlobalMaxCommission::<T>::get() == Some(U::get()),
|
||||
"global maximum commission error"
|
||||
);
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 4,
|
||||
"nomination-pools::migration::v4: wrong storage version"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v3 {
|
||||
use super::*;
|
||||
|
||||
/// This migration removes stale bonded-pool metadata, if any.
|
||||
pub struct MigrateToV3<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV3<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::current_storage_version();
|
||||
let onchain = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
if onchain == 2 {
|
||||
log!(
|
||||
info,
|
||||
"Running migration with current storage version {:?} / onchain {:?}",
|
||||
current,
|
||||
onchain
|
||||
);
|
||||
|
||||
let mut metadata_iterated = 0u64;
|
||||
let mut metadata_removed = 0u64;
|
||||
Metadata::<T>::iter_keys()
|
||||
.filter(|id| {
|
||||
metadata_iterated += 1;
|
||||
!BondedPools::<T>::contains_key(&id)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.for_each(|id| {
|
||||
metadata_removed += 1;
|
||||
Metadata::<T>::remove(&id);
|
||||
});
|
||||
StorageVersion::new(3).put::<Pallet<T>>();
|
||||
// metadata iterated + bonded pools read + a storage version read
|
||||
let total_reads = metadata_iterated * 2 + 1;
|
||||
// metadata removed + a storage version write
|
||||
let total_writes = metadata_removed + 1;
|
||||
T::DbWeight::get().reads_writes(total_reads, total_writes)
|
||||
} else {
|
||||
log!(info, "MigrateToV3 should be removed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
ensure!(
|
||||
Metadata::<T>::iter_keys().all(|id| BondedPools::<T>::contains_key(&id)),
|
||||
"not all of the stale metadata has been removed"
|
||||
);
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 3,
|
||||
"nomination-pools::migration::v3: wrong storage version"
|
||||
);
|
||||
Pallet::<T>::try_state(frame_system::Pallet::<T>::block_number())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -127,7 +418,7 @@ pub mod v2 {
|
||||
use crate::mock::*;
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let join = |x| {
|
||||
Balances::make_free_balance_be(&x, Balances::minimum_balance() + 10);
|
||||
Currency::set_balance(&x, Balances::minimum_balance() + 10);
|
||||
frame_support::assert_ok!(Pools::join(RuntimeOrigin::signed(x), 10, 1));
|
||||
};
|
||||
|
||||
@@ -279,7 +570,7 @@ pub mod v2 {
|
||||
&reward_account,
|
||||
&who,
|
||||
last_claim,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
Preservation::Preserve,
|
||||
);
|
||||
|
||||
if let Err(reason) = outcome {
|
||||
@@ -304,7 +595,7 @@ pub mod v2 {
|
||||
&reward_account,
|
||||
&bonded_pool.roles.depositor,
|
||||
leftover,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
Preservation::Preserve,
|
||||
);
|
||||
log!(warn, "paying {:?} leftover to the depositor: {:?}", leftover, o);
|
||||
}
|
||||
@@ -362,7 +653,7 @@ pub mod v2 {
|
||||
// all reward accounts must have more than ED.
|
||||
RewardPools::<T>::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> {
|
||||
ensure!(
|
||||
T::Currency::free_balance(&Pallet::<T>::create_reward_account(id)) >=
|
||||
<T::Currency as frame_support::traits::fungible::Inspect<T::AccountId>>::balance(&Pallet::<T>::create_reward_account(id)) >=
|
||||
T::Currency::minimum_balance(),
|
||||
"Reward accounts must have greater balance than ED."
|
||||
);
|
||||
@@ -406,109 +697,55 @@ pub mod v2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v3 {
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
|
||||
/// This migration removes stale bonded-pool metadata, if any.
|
||||
pub struct MigrateToV3<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV3<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::current_storage_version();
|
||||
let onchain = Pallet::<T>::on_chain_storage_version();
|
||||
#[derive(Decode)]
|
||||
pub struct OldPoolRoles<AccountId> {
|
||||
pub depositor: AccountId,
|
||||
pub root: AccountId,
|
||||
pub nominator: AccountId,
|
||||
pub bouncer: AccountId,
|
||||
}
|
||||
|
||||
if onchain == 2 {
|
||||
log!(
|
||||
info,
|
||||
"Running migration with current storage version {:?} / onchain {:?}",
|
||||
current,
|
||||
onchain
|
||||
);
|
||||
|
||||
let mut metadata_iterated = 0u64;
|
||||
let mut metadata_removed = 0u64;
|
||||
Metadata::<T>::iter_keys()
|
||||
.filter(|id| {
|
||||
metadata_iterated += 1;
|
||||
!BondedPools::<T>::contains_key(&id)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.for_each(|id| {
|
||||
metadata_removed += 1;
|
||||
Metadata::<T>::remove(&id);
|
||||
});
|
||||
StorageVersion::new(3).put::<Pallet<T>>();
|
||||
// metadata iterated + bonded pools read + a storage version read
|
||||
let total_reads = metadata_iterated * 2 + 1;
|
||||
// metadata removed + a storage version write
|
||||
let total_writes = metadata_removed + 1;
|
||||
T::DbWeight::get().reads_writes(total_reads, total_writes)
|
||||
} else {
|
||||
log!(info, "MigrateToV3 should be removed");
|
||||
T::DbWeight::get().reads(1)
|
||||
impl<AccountId> OldPoolRoles<AccountId> {
|
||||
fn migrate_to_v1(self) -> PoolRoles<AccountId> {
|
||||
PoolRoles {
|
||||
depositor: self.depositor,
|
||||
root: Some(self.root),
|
||||
nominator: Some(self.nominator),
|
||||
bouncer: Some(self.bouncer),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
ensure!(
|
||||
Metadata::<T>::iter_keys().all(|id| BondedPools::<T>::contains_key(&id)),
|
||||
"not all of the stale metadata has been removed"
|
||||
);
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 3,
|
||||
"nomination-pools::migration::v3: wrong storage version"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v4 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Decode)]
|
||||
pub struct OldBondedPoolInner<T: Config> {
|
||||
pub points: BalanceOf<T>,
|
||||
pub state: PoolState,
|
||||
pub member_counter: u32,
|
||||
pub roles: PoolRoles<T::AccountId>,
|
||||
pub roles: OldPoolRoles<T::AccountId>,
|
||||
}
|
||||
|
||||
impl<T: Config> OldBondedPoolInner<T> {
|
||||
fn migrate_to_v4(self) -> BondedPoolInner<T> {
|
||||
fn migrate_to_v1(self) -> BondedPoolInner<T> {
|
||||
// Note: `commission` field not introduced to `BondedPoolInner` until
|
||||
// migration 4.
|
||||
BondedPoolInner {
|
||||
points: self.points,
|
||||
commission: Commission::default(),
|
||||
member_counter: self.member_counter,
|
||||
points: self.points,
|
||||
state: self.state,
|
||||
roles: self.roles,
|
||||
roles: self.roles.migrate_to_v1(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates from `v3` directly to `v5` to avoid the broken `v4` migration.
|
||||
#[allow(deprecated)]
|
||||
pub type MigrateV3ToV5<T, U> = (v4::MigrateToV4<T, U>, v5::MigrateToV5<T>);
|
||||
|
||||
/// # Warning
|
||||
/// Trivial migration which makes the roles of each pool optional.
|
||||
///
|
||||
/// To avoid mangled storage please use `MigrateV3ToV5` instead.
|
||||
/// See: github.com/paritytech/substrate/pull/13715
|
||||
///
|
||||
/// This migration adds a `commission` field to every `BondedPoolInner`, if
|
||||
/// any.
|
||||
#[deprecated(
|
||||
note = "To avoid mangled storage please use `MigrateV3ToV5` instead. See: github.com/paritytech/substrate/pull/13715"
|
||||
)]
|
||||
pub struct MigrateToV4<T, U>(sp_std::marker::PhantomData<(T, U)>);
|
||||
#[allow(deprecated)]
|
||||
impl<T: Config, U: Get<Perbill>> OnRuntimeUpgrade for MigrateToV4<T, U> {
|
||||
/// Note: The depositor is not optional since they can never change.
|
||||
pub struct MigrateToV1<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::current_storage_version();
|
||||
let onchain = Pallet::<T>::on_chain_storage_version();
|
||||
@@ -520,207 +757,33 @@ pub mod v4 {
|
||||
onchain
|
||||
);
|
||||
|
||||
if onchain == 3 {
|
||||
log!(warn, "Please run MigrateToV5 immediately after this migration. See github.com/paritytech/substrate/pull/13715");
|
||||
let initial_global_max_commission = U::get();
|
||||
GlobalMaxCommission::<T>::set(Some(initial_global_max_commission));
|
||||
log!(
|
||||
info,
|
||||
"Set initial global max commission to {:?}.",
|
||||
initial_global_max_commission
|
||||
);
|
||||
|
||||
if current == 1 && onchain == 0 {
|
||||
// this is safe to execute on any runtime that has a bounded number of pools.
|
||||
let mut translated = 0u64;
|
||||
BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
|
||||
translated.saturating_inc();
|
||||
Some(old_value.migrate_to_v4())
|
||||
Some(old_value.migrate_to_v1())
|
||||
});
|
||||
|
||||
StorageVersion::new(4).put::<Pallet<T>>();
|
||||
current.put::<Pallet<T>>();
|
||||
|
||||
log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
|
||||
|
||||
// reads: translated + onchain version.
|
||||
// writes: translated + current.put + initial global commission.
|
||||
T::DbWeight::get().reads_writes(translated + 1, translated + 2)
|
||||
T::DbWeight::get().reads_writes(translated + 1, translated + 1)
|
||||
} else {
|
||||
log!(info, "Migration did not execute. This probably should be removed");
|
||||
log!(info, "Migration did not executed. This probably should be removed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
// ensure all BondedPools items now contain an `inner.commission: Commission` field.
|
||||
// new version must be set.
|
||||
ensure!(
|
||||
BondedPools::<T>::iter().all(|(_, inner)|
|
||||
// Check current
|
||||
(inner.commission.current.is_none() ||
|
||||
inner.commission.current.is_some()) &&
|
||||
// Check max
|
||||
(inner.commission.max.is_none() || inner.commission.max.is_some()) &&
|
||||
// Check change_rate
|
||||
(inner.commission.change_rate.is_none() ||
|
||||
inner.commission.change_rate.is_some()) &&
|
||||
// Check throttle_from
|
||||
(inner.commission.throttle_from.is_none() ||
|
||||
inner.commission.throttle_from.is_some())),
|
||||
"a commission value has not been set correctly"
|
||||
);
|
||||
ensure!(
|
||||
GlobalMaxCommission::<T>::get() == Some(U::get()),
|
||||
"global maximum commission error"
|
||||
);
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 4,
|
||||
"nomination-pools::migration::v4: wrong storage version"
|
||||
Pallet::<T>::on_chain_storage_version() == 1,
|
||||
"The onchain version must be updated after the migration."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v5 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Decode)]
|
||||
pub struct OldRewardPool<T: Config> {
|
||||
last_recorded_reward_counter: T::RewardCounter,
|
||||
last_recorded_total_payouts: BalanceOf<T>,
|
||||
total_rewards_claimed: BalanceOf<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> OldRewardPool<T> {
|
||||
fn migrate_to_v5(self) -> RewardPool<T> {
|
||||
RewardPool {
|
||||
last_recorded_reward_counter: self.last_recorded_reward_counter,
|
||||
last_recorded_total_payouts: self.last_recorded_total_payouts,
|
||||
total_rewards_claimed: self.total_rewards_claimed,
|
||||
total_commission_pending: Zero::zero(),
|
||||
total_commission_claimed: Zero::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This migration adds `total_commission_pending` and `total_commission_claimed` field to every
|
||||
/// `RewardPool`, if any.
|
||||
pub struct MigrateToV5<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV5<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::current_storage_version();
|
||||
let onchain = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log!(
|
||||
info,
|
||||
"Running migration with current storage version {:?} / onchain {:?}",
|
||||
current,
|
||||
onchain
|
||||
);
|
||||
|
||||
if current == 5 && onchain == 4 {
|
||||
let mut translated = 0u64;
|
||||
RewardPools::<T>::translate::<OldRewardPool<T>, _>(|_id, old_value| {
|
||||
translated.saturating_inc();
|
||||
Some(old_value.migrate_to_v5())
|
||||
});
|
||||
|
||||
current.put::<Pallet<T>>();
|
||||
log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
|
||||
|
||||
// reads: translated + onchain version.
|
||||
// writes: translated + current.put.
|
||||
T::DbWeight::get().reads_writes(translated + 1, translated + 1)
|
||||
} else {
|
||||
log!(info, "Migration did not execute. This probably should be removed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let rpool_keys = RewardPools::<T>::iter_keys().count();
|
||||
let rpool_values = RewardPools::<T>::iter_values().count();
|
||||
if rpool_keys != rpool_values {
|
||||
log!(info, "🔥 There are {} undecodable RewardPools in storage. This migration will try to correct them. keys: {}, values: {}", rpool_keys.saturating_sub(rpool_values), rpool_keys, rpool_values);
|
||||
}
|
||||
|
||||
ensure!(
|
||||
PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
|
||||
"There are undecodable PoolMembers in storage. This migration will not fix that."
|
||||
);
|
||||
ensure!(
|
||||
BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
|
||||
"There are undecodable BondedPools in storage. This migration will not fix that."
|
||||
);
|
||||
ensure!(
|
||||
SubPoolsStorage::<T>::iter_keys().count() ==
|
||||
SubPoolsStorage::<T>::iter_values().count(),
|
||||
"There are undecodable SubPools in storage. This migration will not fix that."
|
||||
);
|
||||
ensure!(
|
||||
Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
|
||||
"There are undecodable Metadata in storage. This migration will not fix that."
|
||||
);
|
||||
|
||||
Ok((rpool_values as u64).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let old_rpool_values: u64 = Decode::decode(&mut &data[..]).unwrap();
|
||||
let rpool_keys = RewardPools::<T>::iter_keys().count() as u64;
|
||||
let rpool_values = RewardPools::<T>::iter_values().count() as u64;
|
||||
ensure!(
|
||||
rpool_keys == rpool_values,
|
||||
"There are STILL undecodable RewardPools - migration failed"
|
||||
);
|
||||
|
||||
if old_rpool_values != rpool_values {
|
||||
log!(
|
||||
info,
|
||||
"🎉 Fixed {} undecodable RewardPools.",
|
||||
rpool_values.saturating_sub(old_rpool_values)
|
||||
);
|
||||
}
|
||||
|
||||
// ensure all RewardPools items now contain `total_commission_pending` and
|
||||
// `total_commission_claimed` field.
|
||||
ensure!(
|
||||
RewardPools::<T>::iter().all(|(_, reward_pool)| reward_pool
|
||||
.total_commission_pending >=
|
||||
Zero::zero() && reward_pool
|
||||
.total_commission_claimed >=
|
||||
Zero::zero()),
|
||||
"a commission value has been incorrectly set"
|
||||
);
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= 5,
|
||||
"nomination-pools::migration::v5: wrong storage version"
|
||||
);
|
||||
|
||||
// These should not have been touched - just in case.
|
||||
ensure!(
|
||||
PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
|
||||
"There are undecodable PoolMembers in storage."
|
||||
);
|
||||
ensure!(
|
||||
BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
|
||||
"There are undecodable BondedPools in storage."
|
||||
);
|
||||
ensure!(
|
||||
SubPoolsStorage::<T>::iter_keys().count() ==
|
||||
SubPoolsStorage::<T>::iter_values().count(),
|
||||
"There are undecodable SubPools in storage."
|
||||
);
|
||||
ensure!(
|
||||
Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
|
||||
"There are undecodable Metadata in storage."
|
||||
);
|
||||
|
||||
Pallet::<T>::try_state(frame_system::Pallet::<T>::block_number())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
use super::*;
|
||||
use crate::{self as pools};
|
||||
use frame_support::{assert_ok, parameter_types, PalletId};
|
||||
use frame_support::{assert_ok, parameter_types, traits::fungible::Mutate, PalletId};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::{BuildStorage, FixedU128};
|
||||
use sp_staking::Stake;
|
||||
@@ -29,6 +29,7 @@ pub type RewardCounter = FixedU128;
|
||||
// This sneaky little hack allows us to write code exactly as we would do in the pallet in the tests
|
||||
// as well, e.g. `StorageItem::<T>::get()`.
|
||||
pub type T = Runtime;
|
||||
pub type Currency = <T as Config>::Currency;
|
||||
|
||||
// Ext builder creates a pool with id 1.
|
||||
pub fn default_bonded_account() -> AccountId {
|
||||
@@ -51,8 +52,8 @@ parameter_types! {
|
||||
pub static StakingMinBond: Balance = 10;
|
||||
pub storage Nominations: Option<Vec<AccountId>> = None;
|
||||
}
|
||||
|
||||
pub struct StakingMock;
|
||||
|
||||
impl StakingMock {
|
||||
pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) {
|
||||
let mut x = BondedBalanceMap::get();
|
||||
@@ -221,8 +222,8 @@ impl pallet_balances::Config for Runtime {
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type FreezeIdentifier = RuntimeFreezeReason;
|
||||
type MaxFreezes = ConstU32<1>;
|
||||
type RuntimeHoldReason = ();
|
||||
type MaxHolds = ();
|
||||
}
|
||||
@@ -251,6 +252,7 @@ impl pools::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Currency = Balances;
|
||||
type RuntimeFreezeReason = RuntimeFreezeReason;
|
||||
type RewardCounter = RewardCounter;
|
||||
type BalanceToU256 = BalanceToU256;
|
||||
type U256ToBalance = U256ToBalance;
|
||||
@@ -268,7 +270,7 @@ frame_support::construct_runtime!(
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Storage, Event<T>, Config<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
Pools: pools::{Pallet, Call, Storage, Event<T>},
|
||||
Pools: pools::{Pallet, Call, Storage, Event<T>, FreezeReason},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -356,12 +358,12 @@ impl ExtBuilder {
|
||||
|
||||
// make a pool
|
||||
let amount_to_bond = Pools::depositor_min_bond();
|
||||
Balances::make_free_balance_be(&10, amount_to_bond * 5);
|
||||
Currency::set_balance(&10, amount_to_bond * 5);
|
||||
assert_ok!(Pools::create(RawOrigin::Signed(10).into(), amount_to_bond, 900, 901, 902));
|
||||
assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1]));
|
||||
let last_pool = LastPoolId::<Runtime>::get();
|
||||
for (account_id, bonded) in self.members {
|
||||
Balances::make_free_balance_be(&account_id, bonded * 2);
|
||||
<Runtime as Config>::Currency::set_balance(&account_id, bonded * 2);
|
||||
assert_ok!(Pools::join(RawOrigin::Signed(account_id).into(), bonded, last_pool));
|
||||
}
|
||||
});
|
||||
@@ -440,6 +442,58 @@ pub fn fully_unbond_permissioned(member: AccountId) -> DispatchResult {
|
||||
Pools::unbond(RuntimeOrigin::signed(member), member, points)
|
||||
}
|
||||
|
||||
pub fn pending_rewards_for_delegator(delegator: AccountId) -> Balance {
|
||||
let member = PoolMembers::<T>::get(delegator).unwrap();
|
||||
let bonded_pool = BondedPools::<T>::get(member.pool_id).unwrap();
|
||||
let reward_pool = RewardPools::<T>::get(member.pool_id).unwrap();
|
||||
|
||||
assert!(!bonded_pool.points.is_zero());
|
||||
|
||||
let commission = bonded_pool.commission.current();
|
||||
let current_rc = reward_pool
|
||||
.current_reward_counter(member.pool_id, bonded_pool.points, commission)
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
member.pending_rewards(current_rc).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum RewardImbalance {
|
||||
// There is no reward deficit.
|
||||
Surplus(Balance),
|
||||
// There is a reward deficit.
|
||||
Deficit(Balance),
|
||||
}
|
||||
|
||||
pub fn pool_pending_rewards(pool: PoolId) -> Result<BalanceOf<T>, sp_runtime::DispatchError> {
|
||||
let bonded_pool = BondedPools::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
|
||||
let reward_pool = RewardPools::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
|
||||
|
||||
let current_rc = if !bonded_pool.points.is_zero() {
|
||||
let commission = bonded_pool.commission.current();
|
||||
reward_pool.current_reward_counter(pool, bonded_pool.points, commission)?.0
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
Ok(PoolMembers::<T>::iter()
|
||||
.filter(|(_, d)| d.pool_id == pool)
|
||||
.map(|(_, d)| d.pending_rewards(current_rc).unwrap_or_default())
|
||||
.fold(0u32.into(), |acc: BalanceOf<T>, x| acc.saturating_add(x)))
|
||||
}
|
||||
|
||||
pub fn reward_imbalance(pool: PoolId) -> RewardImbalance {
|
||||
let pending_rewards = pool_pending_rewards(pool).expect("pool should exist");
|
||||
let current_balance = RewardPool::<Runtime>::current_balance(pool);
|
||||
|
||||
if pending_rewards > current_balance {
|
||||
RewardImbalance::Deficit(pending_rewards - current_balance)
|
||||
} else {
|
||||
RewardImbalance::Surplus(current_balance - pending_rewards)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+843
-807
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user