[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:
Ankan
2023-09-29 17:48:40 +02:00
committed by GitHub
parent 0691c91e15
commit f820dc0a1f
13 changed files with 2320 additions and 1798 deletions
+5 -3
View File
@@ -292,9 +292,9 @@ impl pallet_balances::Config for Runtime {
type ReserveIdentifier = [u8; 8]; type ReserveIdentifier = [u8; 8];
type WeightInfo = weights::pallet_balances::WeightInfo<Runtime>; type WeightInfo = weights::pallet_balances::WeightInfo<Runtime>;
type RuntimeHoldReason = RuntimeHoldReason; type RuntimeHoldReason = RuntimeHoldReason;
type FreezeIdentifier = (); type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = ConstU32<1>;
type MaxHolds = ConstU32<1>; type MaxHolds = ConstU32<1>;
type MaxFreezes = ConstU32<0>;
} }
parameter_types! { parameter_types! {
@@ -1311,6 +1311,7 @@ impl pallet_nomination_pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type WeightInfo = weights::pallet_nomination_pools::WeightInfo<Self>; type WeightInfo = weights::pallet_nomination_pools::WeightInfo<Self>;
type Currency = Balances; type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = FixedU128; type RewardCounter = FixedU128;
type BalanceToU256 = BalanceToU256; type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance; type U256ToBalance = U256ToBalance;
@@ -1398,7 +1399,7 @@ construct_runtime! {
VoterList: pallet_bags_list::<Instance1>::{Pallet, Call, Storage, Event<T>} = 25, VoterList: pallet_bags_list::<Instance1>::{Pallet, Call, Storage, Event<T>} = 25,
// Nomination pools for staking. // Nomination pools for staking.
NominationPools: pallet_nomination_pools::{Pallet, Call, Storage, Event<T>, Config<T>} = 29, NominationPools: pallet_nomination_pools::{Pallet, Call, Storage, Event<T>, Config<T>, FreezeReason} = 29,
// Fast unstake pallet: extension to staking. // Fast unstake pallet: extension to staking.
FastUnstake: pallet_fast_unstake = 30, FastUnstake: pallet_fast_unstake = 30,
@@ -1505,6 +1506,7 @@ pub mod migrations {
UpgradeSessionKeys, UpgradeSessionKeys,
parachains_configuration::migration::v9::MigrateToV9<Runtime>, parachains_configuration::migration::v9::MigrateToV9<Runtime>,
paras_registrar::migration::VersionCheckedMigrateToV1<Runtime, ()>, paras_registrar::migration::VersionCheckedMigrateToV1<Runtime, ()>,
pallet_nomination_pools::migration::versioned_migrations::V5toV6<Runtime>,
pallet_referenda::migration::v1::MigrateV0ToV1<Runtime, ()>, pallet_referenda::migration::v1::MigrateV0ToV1<Runtime, ()>,
); );
} }
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
# Schema: Parity PR Documentation Schema (prdoc)
# See doc at https://github.com/paritytech/prdoc
title: Fix for Reward Deficit in the pool
doc:
- audience: Core Dev
description: 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.
notes:
- Introduces new call `adjust_pool_deposit` that allows to top up the deficit or withdraw the excess deposit for the pool.
- Switch to using Fungible trait from Currency trait.
migrations:
db: []
runtime:
- { pallet: "pallet-nomination-pools", description: "One time migration of freezing ED from each of the existing pools."}
crates:
- name: pallet-nomination-pools
host_functions: []
+3 -2
View File
@@ -521,8 +521,8 @@ impl pallet_balances::Config for Runtime {
type ExistentialDeposit = ExistentialDeposit; type ExistentialDeposit = ExistentialDeposit;
type AccountStore = frame_system::Pallet<Runtime>; type AccountStore = frame_system::Pallet<Runtime>;
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>; type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
type FreezeIdentifier = (); type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = (); type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = RuntimeHoldReason; type RuntimeHoldReason = RuntimeHoldReason;
type MaxHolds = ConstU32<2>; type MaxHolds = ConstU32<2>;
} }
@@ -882,6 +882,7 @@ impl pallet_nomination_pools::Config for Runtime {
type WeightInfo = (); type WeightInfo = ();
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type Currency = Balances; type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = FixedU128; type RewardCounter = FixedU128;
type BalanceToU256 = BalanceToU256; type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance; type U256ToBalance = U256ToBalance;
@@ -27,7 +27,10 @@ use frame_benchmarking::v1::{account, whitelist_account};
use frame_election_provider_support::SortedListProvider; use frame_election_provider_support::SortedListProvider;
use frame_support::{ use frame_support::{
assert_ok, ensure, assert_ok, ensure,
traits::{Currency, Get}, traits::{
fungible::{Inspect, Mutate, Unbalanced},
Get,
},
}; };
use frame_system::RawOrigin as RuntimeOrigin; use frame_system::RawOrigin as RuntimeOrigin;
use pallet_nomination_pools::{ use pallet_nomination_pools::{
@@ -67,7 +70,7 @@ fn create_funded_user_with_balance<T: pallet_nomination_pools::Config>(
balance: BalanceOf<T>, balance: BalanceOf<T>,
) -> T::AccountId { ) -> T::AccountId {
let user = account(string, n, USER_SEED); let user = account(string, n, USER_SEED);
T::Currency::make_free_balance_be(&user, balance); T::Currency::set_balance(&user, balance);
user user
} }
@@ -148,8 +151,7 @@ impl<T: Config> ListScenario<T> {
); );
// Burn the entire issuance. // Burn the entire issuance.
let i = CurrencyOf::<T>::burn(CurrencyOf::<T>::total_issuance()); CurrencyOf::<T>::set_total_issuance(Zero::zero());
sp_std::mem::forget(i);
// Create accounts with the origin weight // Create accounts with the origin weight
let (pool_creator1, pool_origin1) = let (pool_creator1, pool_origin1) =
@@ -206,7 +208,7 @@ impl<T: Config> ListScenario<T> {
let joiner: T::AccountId = account("joiner", USER_SEED, 0); let joiner: T::AccountId = account("joiner", USER_SEED, 0);
self.origin1_member = Some(joiner.clone()); self.origin1_member = Some(joiner.clone());
CurrencyOf::<T>::make_free_balance_be(&joiner, amount * 2u32.into()); CurrencyOf::<T>::set_balance(&joiner, amount * 2u32.into());
let original_bonded = T::Staking::active_stake(&self.origin1).unwrap(); let original_bonded = T::Staking::active_stake(&self.origin1).unwrap();
@@ -254,7 +256,7 @@ frame_benchmarking::benchmarks! {
whitelist_account!(joiner); whitelist_account!(joiner);
}: _(RuntimeOrigin::Signed(joiner.clone()), max_additional, 1) }: _(RuntimeOrigin::Signed(joiner.clone()), max_additional, 1)
verify { verify {
assert_eq!(CurrencyOf::<T>::free_balance(&joiner), joiner_free - max_additional); assert_eq!(CurrencyOf::<T>::balance(&joiner), joiner_free - max_additional);
assert_eq!( assert_eq!(
T::Staking::active_stake(&scenario.origin1).unwrap(), T::Staking::active_stake(&scenario.origin1).unwrap(),
scenario.dest_weight scenario.dest_weight
@@ -289,7 +291,7 @@ frame_benchmarking::benchmarks! {
// transfer exactly `extra` to the depositor of the src pool (1), // transfer exactly `extra` to the depositor of the src pool (1),
let reward_account1 = Pools::<T>::create_reward_account(1); let reward_account1 = Pools::<T>::create_reward_account(1);
assert!(extra >= CurrencyOf::<T>::minimum_balance()); assert!(extra >= CurrencyOf::<T>::minimum_balance());
CurrencyOf::<T>::deposit_creating(&reward_account1, extra); let _ = CurrencyOf::<T>::mint_into(&reward_account1, extra);
}: _(RuntimeOrigin::Signed(claimer), T::Lookup::unlookup(scenario.creator1.clone()), BondExtra::Rewards) }: _(RuntimeOrigin::Signed(claimer), T::Lookup::unlookup(scenario.creator1.clone()), BondExtra::Rewards)
verify { verify {
@@ -309,7 +311,7 @@ frame_benchmarking::benchmarks! {
let reward_account = Pools::<T>::create_reward_account(1); let reward_account = Pools::<T>::create_reward_account(1);
// Send funds to the reward account of the pool // Send funds to the reward account of the pool
CurrencyOf::<T>::make_free_balance_be(&reward_account, ed + origin_weight); CurrencyOf::<T>::set_balance(&reward_account, ed + origin_weight);
// set claim preferences to `PermissionlessAll` so any account can claim rewards on member's // set claim preferences to `PermissionlessAll` so any account can claim rewards on member's
// behalf. // behalf.
@@ -317,7 +319,7 @@ frame_benchmarking::benchmarks! {
// Sanity check // Sanity check
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&depositor), CurrencyOf::<T>::balance(&depositor),
origin_weight origin_weight
); );
@@ -325,11 +327,11 @@ frame_benchmarking::benchmarks! {
}:claim_payout_other(RuntimeOrigin::Signed(claimer), depositor.clone()) }:claim_payout_other(RuntimeOrigin::Signed(claimer), depositor.clone())
verify { verify {
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&depositor), CurrencyOf::<T>::balance(&depositor),
origin_weight + commission * origin_weight origin_weight + commission * origin_weight
); );
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&reward_account), CurrencyOf::<T>::balance(&reward_account),
ed + commission * origin_weight ed + commission * origin_weight
); );
} }
@@ -383,7 +385,7 @@ frame_benchmarking::benchmarks! {
T::Staking::active_stake(&pool_account).unwrap(), T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond + min_join_bond min_create_bond + min_join_bond
); );
assert_eq!(CurrencyOf::<T>::free_balance(&joiner), min_join_bond); assert_eq!(CurrencyOf::<T>::balance(&joiner), min_join_bond);
// Unbond the new member // Unbond the new member
Pools::<T>::fully_unbond(RuntimeOrigin::Signed(joiner.clone()).into(), joiner.clone()).unwrap(); Pools::<T>::fully_unbond(RuntimeOrigin::Signed(joiner.clone()).into(), joiner.clone()).unwrap();
@@ -403,7 +405,7 @@ frame_benchmarking::benchmarks! {
}: _(RuntimeOrigin::Signed(pool_account.clone()), 1, s) }: _(RuntimeOrigin::Signed(pool_account.clone()), 1, s)
verify { verify {
// The joiners funds didn't change // The joiners funds didn't change
assert_eq!(CurrencyOf::<T>::free_balance(&joiner), min_join_bond); assert_eq!(CurrencyOf::<T>::balance(&joiner), min_join_bond);
// The unlocking chunk was removed // The unlocking chunk was removed
assert_eq!(pallet_staking::Ledger::<T>::get(pool_account).unwrap().unlocking.len(), 0); assert_eq!(pallet_staking::Ledger::<T>::get(pool_account).unwrap().unlocking.len(), 0);
} }
@@ -426,7 +428,7 @@ frame_benchmarking::benchmarks! {
T::Staking::active_stake(&pool_account).unwrap(), T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond + min_join_bond min_create_bond + min_join_bond
); );
assert_eq!(CurrencyOf::<T>::free_balance(&joiner), min_join_bond); assert_eq!(CurrencyOf::<T>::balance(&joiner), min_join_bond);
// Unbond the new member // Unbond the new member
pallet_staking::CurrentEra::<T>::put(0); pallet_staking::CurrentEra::<T>::put(0);
@@ -447,8 +449,7 @@ frame_benchmarking::benchmarks! {
}: withdraw_unbonded(RuntimeOrigin::Signed(joiner.clone()), joiner_lookup, s) }: withdraw_unbonded(RuntimeOrigin::Signed(joiner.clone()), joiner_lookup, s)
verify { verify {
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&joiner), CurrencyOf::<T>::balance(&joiner), min_join_bond * 2u32.into()
min_join_bond * 2u32.into()
); );
// The unlocking chunk was removed // The unlocking chunk was removed
assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 0); assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 0);
@@ -485,7 +486,7 @@ frame_benchmarking::benchmarks! {
Zero::zero() Zero::zero()
); );
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&pool_account), CurrencyOf::<T>::balance(&pool_account),
min_create_bond min_create_bond
); );
assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 1); assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 1);
@@ -515,7 +516,7 @@ frame_benchmarking::benchmarks! {
// Funds where transferred back correctly // Funds where transferred back correctly
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&depositor), CurrencyOf::<T>::balance(&depositor),
// gets bond back + rewards collecting when unbonding // gets bond back + rewards collecting when unbonding
min_create_bond * 2u32.into() + CurrencyOf::<T>::minimum_balance() min_create_bond * 2u32.into() + CurrencyOf::<T>::minimum_balance()
); );
@@ -527,7 +528,7 @@ frame_benchmarking::benchmarks! {
let depositor_lookup = T::Lookup::unlookup(depositor.clone()); let depositor_lookup = T::Lookup::unlookup(depositor.clone());
// Give the depositor some balance to bond // Give the depositor some balance to bond
CurrencyOf::<T>::make_free_balance_be(&depositor, min_create_bond * 2u32.into()); CurrencyOf::<T>::set_balance(&depositor, min_create_bond * 2u32.into());
// Make sure no Pools exist at a pre-condition for our verify checks // Make sure no Pools exist at a pre-condition for our verify checks
assert_eq!(RewardPools::<T>::count(), 0); assert_eq!(RewardPools::<T>::count(), 0);
@@ -782,7 +783,7 @@ frame_benchmarking::benchmarks! {
let ed = CurrencyOf::<T>::minimum_balance(); let ed = CurrencyOf::<T>::minimum_balance();
let (depositor, pool_account) = create_pool_account::<T>(0, origin_weight, Some(commission)); let (depositor, pool_account) = create_pool_account::<T>(0, origin_weight, Some(commission));
let reward_account = Pools::<T>::create_reward_account(1); let reward_account = Pools::<T>::create_reward_account(1);
CurrencyOf::<T>::make_free_balance_be(&reward_account, ed + origin_weight); CurrencyOf::<T>::set_balance(&reward_account, ed + origin_weight);
// member claims a payout to make some commission available. // member claims a payout to make some commission available.
let _ = Pools::<T>::claim_payout(RuntimeOrigin::Signed(claimer).into()); let _ = Pools::<T>::claim_payout(RuntimeOrigin::Signed(claimer).into());
@@ -791,15 +792,29 @@ frame_benchmarking::benchmarks! {
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into()) }:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into())
verify { verify {
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&depositor), CurrencyOf::<T>::balance(&depositor),
origin_weight + commission * origin_weight origin_weight + commission * origin_weight
); );
assert_eq!( assert_eq!(
CurrencyOf::<T>::free_balance(&reward_account), CurrencyOf::<T>::balance(&reward_account),
ed + commission * origin_weight ed + commission * origin_weight
); );
} }
adjust_pool_deposit {
// Create a pool
let (depositor, _) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
// Remove ed freeze to create a scenario where the ed deposit needs to be adjusted.
let _ = Pools::<T>::unfreeze_pool_deposit(&Pools::<T>::create_reward_account(1));
assert!(&Pools::<T>::check_ed_imbalance().is_err());
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor), 1)
verify {
assert!(&Pools::<T>::check_ed_imbalance().is_ok());
}
impl_benchmark_test_suite!( impl_benchmark_test_suite!(
Pallet, Pallet,
crate::mock::new_test_ext(), crate::mock::new_test_ext(),
@@ -74,8 +74,8 @@ impl pallet_balances::Config for Runtime {
type ExistentialDeposit = ExistentialDeposit; type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System; type AccountStore = System;
type WeightInfo = (); type WeightInfo = ();
type FreezeIdentifier = (); type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = (); type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = (); type RuntimeHoldReason = ();
type MaxHolds = (); type MaxHolds = ();
} }
@@ -160,6 +160,7 @@ impl pallet_nomination_pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type WeightInfo = (); type WeightInfo = ();
type Currency = Balances; type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = FixedU128; type RewardCounter = FixedU128;
type BalanceToU256 = BalanceToU256; type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance; type U256ToBalance = U256ToBalance;
@@ -183,7 +184,7 @@ frame_support::construct_runtime!(
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
Staking: pallet_staking::{Pallet, Call, Config<T>, Storage, Event<T>}, Staking: pallet_staking::{Pallet, Call, Config<T>, Storage, Event<T>},
VoterList: pallet_bags_list::<Instance1>::{Pallet, Call, Storage, Event<T>}, VoterList: pallet_bags_list::<Instance1>::{Pallet, Call, Storage, Event<T>},
Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event<T>}, Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event<T>, FreezeReason},
} }
); );
+205 -44
View File
@@ -353,12 +353,16 @@
use codec::Codec; use codec::Codec;
use frame_support::{ use frame_support::{
defensive, ensure, defensive, defensive_assert, ensure,
pallet_prelude::{MaxEncodedLen, *}, pallet_prelude::{MaxEncodedLen, *},
storage::bounded_btree_map::BoundedBTreeMap, storage::bounded_btree_map::BoundedBTreeMap,
traits::{ traits::{
Currency, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, fungible::{
ExistenceRequirement, Get, Inspect as FunInspect, InspectFreeze, Mutate as FunMutate,
MutateFreeze as FunMutateFreeze,
},
tokens::{Fortitude, Preservation},
Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get,
}, },
DefaultNoBound, PalletError, DefaultNoBound, PalletError,
}; };
@@ -380,7 +384,6 @@ use sp_runtime::TryRuntimeError;
/// The log target of this pallet. /// The log target of this pallet.
pub const LOG_TARGET: &str = "runtime::nomination-pools"; pub const LOG_TARGET: &str = "runtime::nomination-pools";
// syntactic sugar for logging. // syntactic sugar for logging.
#[macro_export] #[macro_export]
macro_rules! log { macro_rules! log {
@@ -405,7 +408,7 @@ pub use weights::WeightInfo;
/// The balance type used by the currency system. /// The balance type used by the currency system.
pub type BalanceOf<T> = 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. /// Type used for unique identifier of each pool.
pub type PoolId = u32; pub type PoolId = u32;
@@ -1005,10 +1008,15 @@ impl<T: Config> BondedPool<T> {
self self
} }
/// The pools balance that is transferrable. /// The pools balance that is transferable provided it is expendable by staking pallet.
fn transferrable_balance(&self) -> BalanceOf<T> { fn transferable_balance(&self) -> BalanceOf<T> {
let account = self.bonded_account(); 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()) .saturating_sub(T::Staking::active_stake(&account).unwrap_or_default())
} }
@@ -1201,8 +1209,8 @@ impl<T: Config> BondedPool<T> {
&bonded_account, &bonded_account,
amount, amount,
match ty { match ty {
BondType::Create => ExistenceRequirement::AllowDeath, BondType::Create => Preservation::Expendable,
BondType::Later => ExistenceRequirement::KeepAlive, BondType::Later => Preservation::Preserve,
}, },
)?; )?;
// We must calculate the points issued *before* we bond who's funds, else points:balance // 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 =
self.total_commission_pending.saturating_add(new_pending_commission); self.total_commission_pending.saturating_add(new_pending_commission);
// Store the total payouts at the time of this update. Total payouts are essentially the // Total payouts are essentially the entire historical balance of the reward pool, equating
// entire historical balance of the reward pool, equating to the current balance + the total // to the current balance + the total rewards that have left the pool + the total commission
// rewards that have left the pool + the total commission that has left the pool. // that has left the pool.
self.last_recorded_total_payouts = balance let last_recorded_total_payouts = balance
.checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed)) .checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed))
.ok_or(Error::<T>::OverflowRisk)?; .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(()) Ok(())
} }
@@ -1380,8 +1397,11 @@ impl<T: Config> RewardPool<T> {
/// ///
/// This is sum of all the rewards that are claimable by pool members. /// This is sum of all the rewards that are claimable by pool members.
fn current_balance(id: PoolId) -> BalanceOf<T> { fn current_balance(id: PoolId) -> BalanceOf<T> {
T::Currency::free_balance(&Pallet::<T>::create_reward_account(id)) T::Currency::reducible_balance(
.saturating_sub(T::Currency::minimum_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 /// `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. /// improved UX [`Config::PostUnbondingPoolsWindow`] should be configured to a non-zero value.
pub struct TotalUnbondingPools<T: Config>(PhantomData<T>); pub struct TotalUnbondingPools<T: Config>(PhantomData<T>);
impl<T: Config> Get<u32> for TotalUnbondingPools<T> { impl<T: Config> Get<u32> for TotalUnbondingPools<T> {
fn get() -> u32 { fn get() -> u32 {
// NOTE: this may be dangerous in the scenario bonding_duration gets decreased because // NOTE: this may be dangerous in the scenario bonding_duration gets decreased because
@@ -1504,7 +1525,7 @@ pub mod pallet {
use sp_runtime::Perbill; use sp_runtime::Perbill;
/// The current storage version. /// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);
#[pallet::pallet] #[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)] #[pallet::storage_version(STORAGE_VERSION)]
@@ -1518,8 +1539,12 @@ pub mod pallet {
/// Weight information for extrinsics in this pallet. /// Weight information for extrinsics in this pallet.
type WeightInfo: weights::WeightInfo; type WeightInfo: weights::WeightInfo;
/// The nominating balance. /// The currency type used for nomination pool.
type Currency: Currency<Self::AccountId>; 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. /// The type that is used for reward counter.
/// ///
@@ -1685,6 +1710,7 @@ pub mod pallet {
fn build(&self) { fn build(&self) {
MinJoinBond::<T>::put(self.min_join_bond); MinJoinBond::<T>::put(self.min_join_bond);
MinCreateBond::<T>::put(self.min_create_bond); MinCreateBond::<T>::put(self.min_create_bond);
if let Some(max_pools) = self.max_pools { if let Some(max_pools) = self.max_pools {
MaxPools::<T>::put(max_pools); MaxPools::<T>::put(max_pools);
} }
@@ -1770,6 +1796,10 @@ pub mod pallet {
}, },
/// Pool commission has been claimed. /// Pool commission has been claimed.
PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf<T> }, 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] #[pallet::error]
@@ -1845,6 +1875,8 @@ pub mod pallet {
InvalidPoolId, InvalidPoolId,
/// Bonding extra is restricted to the exact pending reward amount. /// Bonding extra is restricted to the exact pending reward amount.
BondExtraRestricted, BondExtraRestricted,
/// No imbalance in the ED deposit for the pool.
NothingToAdjust,
} }
#[derive(Encode, Decode, PartialEq, TypeInfo, PalletError, RuntimeDebug)] #[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] #[pallet::call]
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// Stake funds with a pool. The amount to bond is transferred from the member to the /// 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); ensure!(!withdrawn_points.is_empty(), Error::<T>::CannotWithdrawAny);
// Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the // Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the
// `transferrable_balance` is correct. // `transferable_balance` is correct.
let stash_killed = let stash_killed =
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), num_slashing_spans)?; 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 // 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 // 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. // 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( T::Currency::transfer(
&bonded_pool.bonded_account(), &bonded_pool.bonded_account(),
&member_account, &member_account,
balance_to_unbond, balance_to_unbond,
ExistenceRequirement::AllowDeath, Preservation::Expendable,
) )
.defensive()?; .defensive()?;
@@ -2237,7 +2277,7 @@ pub mod pallet {
/// # Note /// # Note
/// ///
/// In addition to `amount`, the caller will transfer the existential deposit; so the caller /// 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::call_index(6)]
#[pallet::weight(T::WeightInfo::create())] #[pallet::weight(T::WeightInfo::create())]
pub fn create( pub fn create(
@@ -2631,6 +2671,20 @@ pub mod pallet {
let who = ensure_signed(origin)?; let who = ensure_signed(origin)?;
Self::do_claim_commission(who, pool_id) 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] #[pallet::hooks]
@@ -2681,6 +2735,9 @@ impl<T: Config> Pallet<T> {
RewardPools::<T>::remove(bonded_pool.id); RewardPools::<T>::remove(bonded_pool.id);
SubPoolsStorage::<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 // 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 // accounts have no references that would prevent destruction once we get to this point. We
// don't work with the system pallet directly, but // don't work with the system pallet directly, but
@@ -2688,26 +2745,44 @@ impl<T: Config> Pallet<T> {
// consumers anyway. // consumers anyway.
// 2. the bonded account should become a 'killed stash' in the staking system, and all of // 2. the bonded account should become a 'killed stash' in the staking system, and all of
// its consumers removed. // its consumers removed.
debug_assert_eq!(frame_system::Pallet::<T>::consumers(&reward_account), 0); defensive_assert!(
debug_assert_eq!(frame_system::Pallet::<T>::consumers(&bonded_account), 0); frame_system::Pallet::<T>::consumers(&reward_account) == 0,
debug_assert_eq!( "reward account of dissolving pool should have no consumers"
T::Staking::total_stake(&bonded_account).unwrap_or_default(), );
Zero::zero() 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 // 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. // of unclaimed pending commission, erroneous transfers to the reward account, etc.
let reward_pool_remaining = T::Currency::free_balance(&reward_account); let reward_pool_remaining = T::Currency::reducible_balance(
&reward_account,
Preservation::Expendable,
Fortitude::Polite,
);
let _ = T::Currency::transfer( let _ = T::Currency::transfer(
&reward_account, &reward_account,
&bonded_pool.roles.depositor, &bonded_pool.roles.depositor,
reward_pool_remaining, reward_pool_remaining,
ExistenceRequirement::AllowDeath, Preservation::Expendable,
); );
// NOTE: this is purely defensive. defensive_assert!(
T::Currency::make_free_balance_be(&reward_account, Zero::zero()); T::Currency::total_balance(&reward_account) == Zero::zero(),
T::Currency::make_free_balance_be(&bonded_pool.bonded_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 }); Self::deposit_event(Event::<T>::Destroyed { pool_id: bonded_pool.id });
// Remove bonded pool metadata. // Remove bonded pool metadata.
@@ -2838,7 +2913,7 @@ impl<T: Config> Pallet<T> {
pending_rewards, pending_rewards,
// defensive: the depositor has put existential deposit into the pool and it stays // defensive: the depositor has put existential deposit into the pool and it stays
// untouched, reward account shall not die. // untouched, reward account shall not die.
ExistenceRequirement::KeepAlive, Preservation::Preserve,
)?; )?;
Self::deposit_event(Event::<T>::PaidOut { Self::deposit_event(Event::<T>::PaidOut {
@@ -2846,7 +2921,6 @@ impl<T: Config> Pallet<T> {
pool_id: member.pool_id, pool_id: member.pool_id,
payout: pending_rewards, payout: pending_rewards,
}); });
Ok(pending_rewards) Ok(pending_rewards)
} }
@@ -2881,13 +2955,17 @@ impl<T: Config> Pallet<T> {
bonded_pool.try_inc_members()?; bonded_pool.try_inc_members()?;
let points = bonded_pool.try_bond_funds(&who, amount, BondType::Create)?; let points = bonded_pool.try_bond_funds(&who, amount, BondType::Create)?;
// Transfer the minimum balance for the reward account.
T::Currency::transfer( T::Currency::transfer(
&who, &who,
&bonded_pool.reward_account(), &bonded_pool.reward_account(),
T::Currency::minimum_balance(), 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( PoolMembers::<T>::insert(
who.clone(), who.clone(),
PoolMember::<T> { PoolMember::<T> {
@@ -2999,7 +3077,7 @@ impl<T: Config> Pallet<T> {
&bonded_pool.reward_account(), &bonded_pool.reward_account(),
&payee, &payee,
commission, commission,
ExistenceRequirement::KeepAlive, Preservation::Preserve,
)?; )?;
// Add pending commission to total claimed counter. // Add pending commission to total claimed counter.
@@ -3007,7 +3085,6 @@ impl<T: Config> Pallet<T> {
reward_pool.total_commission_claimed.saturating_add(commission); reward_pool.total_commission_claimed.saturating_add(commission);
// Reset total pending commission counter to zero. // Reset total pending commission counter to zero.
reward_pool.total_commission_pending = Zero::zero(); reward_pool.total_commission_pending = Zero::zero();
// Commit reward pool updates
RewardPools::<T>::insert(pool_id, reward_pool); RewardPools::<T>::insert(pool_id, reward_pool);
Self::deposit_event(Event::<T>::PoolCommissionClaimed { pool_id, commission }); Self::deposit_event(Event::<T>::PoolCommissionClaimed { pool_id, commission });
@@ -3029,6 +3106,55 @@ impl<T: Config> Pallet<T> {
Ok(()) 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. /// Ensure the correctness of the state of this pallet.
/// ///
/// This should be valid before or after each state transition 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 { for id in reward_pools {
let account = Self::create_reward_account(id); 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!( log!(
warn, warn,
"reward pool of {:?}: {:?} (ed = {:?}), should only happen because ED has \ "reward pool of {:?}: {:?} (ed = {:?}), should only happen because ED has \
changed recently. Pool operators should be notified to top up the reward \ changed recently. Pool operators should be notified to top up the reward \
account", account",
id, id,
T::Currency::free_balance(&account), T::Currency::reducible_balance(
&account,
Preservation::Expendable,
Fortitude::Polite
),
T::Currency::minimum_balance(), T::Currency::minimum_balance(),
) )
} }
@@ -3135,9 +3267,8 @@ impl<T: Config> Pallet<T> {
let pending_rewards_lt_leftover_bal = RewardPool::<T>::current_balance(id) >= let pending_rewards_lt_leftover_bal = RewardPool::<T>::current_balance(id) >=
pools_members_pending_rewards.get(&id).copied().unwrap_or_default(); pools_members_pending_rewards.get(&id).copied().unwrap_or_default();
// this is currently broken in Kusama, a fix is being worked on in // If this happens, this is most likely due to an old bug and not a recent code change.
// <https://github.com/paritytech/polkadot-sdk/pull/1255>. until it is fixed, log a // We warn about this in try-runtime checks but do not panic.
// warning instead of panicing with an `ensure` statement.
if !pending_rewards_lt_leftover_bal { if !pending_rewards_lt_leftover_bal {
log::warn!( log::warn!(
"pool {:?}, sum pending rewards = {:?}, remaining balance = {:?}", "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(()) 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`. /// 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 /// This is useful for backwards compatibility with the majority of tests that only deal with
+382 -319
View File
@@ -23,55 +23,95 @@ use sp_std::{collections::btree_map::BTreeMap, vec::Vec};
#[cfg(feature = "try-runtime")] #[cfg(feature = "try-runtime")]
use sp_runtime::TryRuntimeError; 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::*; use super::*;
#[derive(Decode)] #[derive(Decode)]
pub struct OldPoolRoles<AccountId> { pub struct OldRewardPool<T: Config> {
pub depositor: AccountId, last_recorded_reward_counter: T::RewardCounter,
pub root: AccountId, last_recorded_total_payouts: BalanceOf<T>,
pub nominator: AccountId, total_rewards_claimed: BalanceOf<T>,
pub bouncer: AccountId,
} }
impl<AccountId> OldPoolRoles<AccountId> { impl<T: Config> OldRewardPool<T> {
fn migrate_to_v1(self) -> PoolRoles<AccountId> { fn migrate_to_v5(self) -> RewardPool<T> {
PoolRoles { RewardPool {
depositor: self.depositor, last_recorded_reward_counter: self.last_recorded_reward_counter,
root: Some(self.root), last_recorded_total_payouts: self.last_recorded_total_payouts,
nominator: Some(self.nominator), total_rewards_claimed: self.total_rewards_claimed,
bouncer: Some(self.bouncer), total_commission_pending: Zero::zero(),
total_commission_claimed: Zero::zero(),
} }
} }
} }
#[derive(Decode)] /// This migration adds `total_commission_pending` and `total_commission_claimed` field to every
pub struct OldBondedPoolInner<T: Config> { /// `RewardPool`, if any.
pub points: BalanceOf<T>, pub struct MigrateToV5<T>(sp_std::marker::PhantomData<T>);
pub state: PoolState, impl<T: Config> OnRuntimeUpgrade for MigrateToV5<T> {
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> {
fn on_runtime_upgrade() -> Weight { fn on_runtime_upgrade() -> Weight {
let current = Pallet::<T>::current_storage_version(); let current = Pallet::<T>::current_storage_version();
let onchain = Pallet::<T>::on_chain_storage_version(); let onchain = Pallet::<T>::on_chain_storage_version();
@@ -83,33 +123,284 @@ pub mod v1 {
onchain onchain
); );
if current == 1 && onchain == 0 { if current == 5 && onchain == 4 {
// this is safe to execute on any runtime that has a bounded number of pools.
let mut translated = 0u64; let mut translated = 0u64;
BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| { RewardPools::<T>::translate::<OldRewardPool<T>, _>(|_id, old_value| {
translated.saturating_inc(); translated.saturating_inc();
Some(old_value.migrate_to_v1()) Some(old_value.migrate_to_v5())
}); });
current.put::<Pallet<T>>(); current.put::<Pallet<T>>();
log!(info, "Upgraded {} pools, storage to version {:?}", translated, current); 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) T::DbWeight::get().reads_writes(translated + 1, translated + 1)
} else { } 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) T::DbWeight::get().reads(1)
} }
} }
#[cfg(feature = "try-runtime")] #[cfg(feature = "try-runtime")]
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> { fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
// new version must be set. 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!( ensure!(
Pallet::<T>::on_chain_storage_version() == 1, PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
"The onchain version must be updated after the migration." "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(()) Ok(())
} }
} }
@@ -127,7 +418,7 @@ pub mod v2 {
use crate::mock::*; use crate::mock::*;
ExtBuilder::default().build_and_execute(|| { ExtBuilder::default().build_and_execute(|| {
let join = |x| { 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)); frame_support::assert_ok!(Pools::join(RuntimeOrigin::signed(x), 10, 1));
}; };
@@ -279,7 +570,7 @@ pub mod v2 {
&reward_account, &reward_account,
&who, &who,
last_claim, last_claim,
ExistenceRequirement::KeepAlive, Preservation::Preserve,
); );
if let Err(reason) = outcome { if let Err(reason) = outcome {
@@ -304,7 +595,7 @@ pub mod v2 {
&reward_account, &reward_account,
&bonded_pool.roles.depositor, &bonded_pool.roles.depositor,
leftover, leftover,
ExistenceRequirement::KeepAlive, Preservation::Preserve,
); );
log!(warn, "paying {:?} leftover to the depositor: {:?}", leftover, o); log!(warn, "paying {:?} leftover to the depositor: {:?}", leftover, o);
} }
@@ -362,7 +653,7 @@ pub mod v2 {
// all reward accounts must have more than ED. // all reward accounts must have more than ED.
RewardPools::<T>::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> { RewardPools::<T>::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> {
ensure!( 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(), T::Currency::minimum_balance(),
"Reward accounts must have greater balance than ED." "Reward accounts must have greater balance than ED."
); );
@@ -406,109 +697,55 @@ pub mod v2 {
} }
} }
pub mod v3 { pub mod v1 {
use super::*; use super::*;
/// This migration removes stale bonded-pool metadata, if any. #[derive(Decode)]
pub struct MigrateToV3<T>(sp_std::marker::PhantomData<T>); pub struct OldPoolRoles<AccountId> {
impl<T: Config> OnRuntimeUpgrade for MigrateToV3<T> { pub depositor: AccountId,
fn on_runtime_upgrade() -> Weight { pub root: AccountId,
let current = Pallet::<T>::current_storage_version(); pub nominator: AccountId,
let onchain = Pallet::<T>::on_chain_storage_version(); pub bouncer: AccountId,
}
if onchain == 2 { impl<AccountId> OldPoolRoles<AccountId> {
log!( fn migrate_to_v1(self) -> PoolRoles<AccountId> {
info, PoolRoles {
"Running migration with current storage version {:?} / onchain {:?}", depositor: self.depositor,
current, root: Some(self.root),
onchain nominator: Some(self.nominator),
); bouncer: Some(self.bouncer),
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"
);
Ok(())
}
} }
}
pub mod v4 {
use super::*;
#[derive(Decode)] #[derive(Decode)]
pub struct OldBondedPoolInner<T: Config> { pub struct OldBondedPoolInner<T: Config> {
pub points: BalanceOf<T>, pub points: BalanceOf<T>,
pub state: PoolState, pub state: PoolState,
pub member_counter: u32, pub member_counter: u32,
pub roles: PoolRoles<T::AccountId>, pub roles: OldPoolRoles<T::AccountId>,
} }
impl<T: Config> OldBondedPoolInner<T> { 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 { BondedPoolInner {
points: self.points,
commission: Commission::default(), commission: Commission::default(),
member_counter: self.member_counter, member_counter: self.member_counter,
points: self.points,
state: self.state, state: self.state,
roles: self.roles, roles: self.roles.migrate_to_v1(),
} }
} }
} }
/// Migrates from `v3` directly to `v5` to avoid the broken `v4` migration. /// Trivial migration which makes the roles of each pool optional.
#[allow(deprecated)]
pub type MigrateV3ToV5<T, U> = (v4::MigrateToV4<T, U>, v5::MigrateToV5<T>);
/// # Warning
/// ///
/// To avoid mangled storage please use `MigrateV3ToV5` instead. /// Note: The depositor is not optional since they can never change.
/// See: github.com/paritytech/substrate/pull/13715 pub struct MigrateToV1<T>(sp_std::marker::PhantomData<T>);
/// impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
/// 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 { fn on_runtime_upgrade() -> Weight {
let current = Pallet::<T>::current_storage_version(); let current = Pallet::<T>::current_storage_version();
let onchain = Pallet::<T>::on_chain_storage_version(); let onchain = Pallet::<T>::on_chain_storage_version();
@@ -520,207 +757,33 @@ pub mod v4 {
onchain onchain
); );
if onchain == 3 { if current == 1 && onchain == 0 {
log!(warn, "Please run MigrateToV5 immediately after this migration. See github.com/paritytech/substrate/pull/13715"); // this is safe to execute on any runtime that has a bounded number of pools.
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; let mut translated = 0u64;
BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| { BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
translated.saturating_inc(); 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); log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
// reads: translated + onchain version. T::DbWeight::get().reads_writes(translated + 1, translated + 1)
// writes: translated + current.put + initial global commission.
T::DbWeight::get().reads_writes(translated + 1, translated + 2)
} else { } 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) T::DbWeight::get().reads(1)
} }
} }
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
Ok(Vec::new())
}
#[cfg(feature = "try-runtime")] #[cfg(feature = "try-runtime")]
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> { fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
// ensure all BondedPools items now contain an `inner.commission: Commission` field. // new version must be set.
ensure!( ensure!(
BondedPools::<T>::iter().all(|(_, inner)| Pallet::<T>::on_chain_storage_version() == 1,
// Check current "The onchain version must be updated after the migration."
(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(()) Pallet::<T>::try_state(frame_system::Pallet::<T>::block_number())?;
}
}
}
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."
);
Ok(()) Ok(())
} }
} }
+61 -7
View File
@@ -17,7 +17,7 @@
use super::*; use super::*;
use crate::{self as pools}; 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 frame_system::RawOrigin;
use sp_runtime::{BuildStorage, FixedU128}; use sp_runtime::{BuildStorage, FixedU128};
use sp_staking::Stake; 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 // 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()`. // as well, e.g. `StorageItem::<T>::get()`.
pub type T = Runtime; pub type T = Runtime;
pub type Currency = <T as Config>::Currency;
// Ext builder creates a pool with id 1. // Ext builder creates a pool with id 1.
pub fn default_bonded_account() -> AccountId { pub fn default_bonded_account() -> AccountId {
@@ -51,8 +52,8 @@ parameter_types! {
pub static StakingMinBond: Balance = 10; pub static StakingMinBond: Balance = 10;
pub storage Nominations: Option<Vec<AccountId>> = None; pub storage Nominations: Option<Vec<AccountId>> = None;
} }
pub struct StakingMock; pub struct StakingMock;
impl StakingMock { impl StakingMock {
pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) { pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) {
let mut x = BondedBalanceMap::get(); let mut x = BondedBalanceMap::get();
@@ -221,8 +222,8 @@ impl pallet_balances::Config for Runtime {
type ExistentialDeposit = ExistentialDeposit; type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System; type AccountStore = System;
type WeightInfo = (); type WeightInfo = ();
type FreezeIdentifier = (); type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = (); type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = (); type RuntimeHoldReason = ();
type MaxHolds = (); type MaxHolds = ();
} }
@@ -251,6 +252,7 @@ impl pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type WeightInfo = (); type WeightInfo = ();
type Currency = Balances; type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = RewardCounter; type RewardCounter = RewardCounter;
type BalanceToU256 = BalanceToU256; type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance; type U256ToBalance = U256ToBalance;
@@ -268,7 +270,7 @@ frame_support::construct_runtime!(
{ {
System: frame_system::{Pallet, Call, Storage, Event<T>, Config<T>}, System: frame_system::{Pallet, Call, Storage, Event<T>, Config<T>},
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<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 // make a pool
let amount_to_bond = Pools::depositor_min_bond(); 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::create(RawOrigin::Signed(10).into(), amount_to_bond, 900, 901, 902));
assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1]));
let last_pool = LastPoolId::<Runtime>::get(); let last_pool = LastPoolId::<Runtime>::get();
for (account_id, bonded) in self.members { 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)); 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) 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -85,8 +85,8 @@ impl pallet_balances::Config for Runtime {
type ExistentialDeposit = ExistentialDeposit; type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System; type AccountStore = System;
type WeightInfo = (); type WeightInfo = ();
type FreezeIdentifier = (); type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = (); type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = (); type RuntimeHoldReason = ();
type MaxHolds = (); type MaxHolds = ();
} }
@@ -174,6 +174,7 @@ impl pallet_nomination_pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type WeightInfo = (); type WeightInfo = ();
type Currency = Balances; type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = FixedU128; type RewardCounter = FixedU128;
type BalanceToU256 = BalanceToU256; type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance; type U256ToBalance = U256ToBalance;
@@ -195,7 +196,7 @@ frame_support::construct_runtime!(
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
Staking: pallet_staking::{Pallet, Call, Config<T>, Storage, Event<T>}, Staking: pallet_staking::{Pallet, Call, Config<T>, Storage, Event<T>},
VoterList: pallet_bags_list::<Instance1>::{Pallet, Call, Storage, Event<T>}, VoterList: pallet_bags_list::<Instance1>::{Pallet, Call, Storage, Event<T>},
Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event<T>}, Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event<T>, FreezeReason},
} }
); );
@@ -20,7 +20,10 @@
use codec::{Decode, Encode, FullCodec, MaxEncodedLen}; use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero}; use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero};
use sp_core::RuntimeDebug; use sp_core::RuntimeDebug;
use sp_runtime::{traits::Convert, ArithmeticError, DispatchError, TokenError}; use sp_runtime::{
traits::{Convert, MaybeSerializeDeserialize},
ArithmeticError, DispatchError, TokenError,
};
use sp_std::fmt::Debug; use sp_std::fmt::Debug;
/// The origin of funds to be used for a deposit operation. /// The origin of funds to be used for a deposit operation.
@@ -240,6 +243,7 @@ pub trait Balance:
+ MaxEncodedLen + MaxEncodedLen
+ Send + Send
+ Sync + Sync
+ MaybeSerializeDeserialize
+ 'static + 'static
{ {
} }
@@ -253,6 +257,7 @@ impl<
+ MaxEncodedLen + MaxEncodedLen
+ Send + Send
+ Sync + Sync
+ MaybeSerializeDeserialize
+ 'static, + 'static,
> Balance for T > Balance for T
{ {