Skip to content

Commit

Permalink
Pools: Add ability to configure commission claiming permissions (#2474)
Browse files Browse the repository at this point in the history
Addresses #409.

This request has been raised by multiple community members - the ability
for the nomination pool root role to configure permissionless commission
claiming:

> Would it be possible to have a claim_commission_other extrinsic for
claiming commission of nomination pools permissionless?

This PR does not quite introduce this additional call, but amends
`do_claim_commission` to check a new `claim_permission` field in the
`Commission` struct, configured by an enum:

```
enum CommissionClaimPermission {
   Permissionless,
   Account(AccountId),
}
```
This can be optionally set in a bonded pool's
`commission.claim_permission` field:

```
struct BondedPool {
   commission: {
      <snip>
      claim_permission: Option<CommissionClaimPermission<T::AccountId>>,
   },
   <snip>
}
```

This is a new field and requires a migration to add it to existing
pools. This will be `None` on pool creation, falling back to the `root`
role having sole access to claim commission if it is not set; this is
the behaviour as it is today. Once set, the field _can_ be set to `None`
again.

#### Changes
- [x] Add `commision.claim_permission` field.
- [x] Add `can_claim_commission` and amend `do_claim_commission`.
- [x] Add `set_commission_claim_permission` call.
- [x] Test to cover new configs and call.
- [x] Add and amend benchmarks.
- [x] Generate new weights + slot into call
`set_commission_claim_permission`.
- [x] Add migration to introduce `commission.claim_permission`, bump
storage version.
- [x] Update Westend weights.
- [x] Migration working.

---------

Co-authored-by: command-bot <>
  • Loading branch information
Ross Bulat authored Nov 28, 2023
1 parent 2ac23d2 commit 7506271
Show file tree
Hide file tree
Showing 7 changed files with 796 additions and 393 deletions.
3 changes: 1 addition & 2 deletions polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1533,11 +1533,10 @@ pub mod migrations {
parachains_configuration::migration::v8::MigrateToV8<Runtime>,
parachains_configuration::migration::v9::MigrateToV9<Runtime>,
paras_registrar::migration::MigrateToV1<Runtime, ()>,
pallet_nomination_pools::migration::versioned::V5toV6<Runtime>,
pallet_referenda::migration::v1::MigrateV0ToV1<Runtime, ()>,
pallet_nomination_pools::migration::versioned::V6ToV7<Runtime>,
pallet_grandpa::migrations::MigrateV4ToV5<Runtime>,
parachains_configuration::migration::v10::MigrateToV10<Runtime>,
pallet_nomination_pools::migration::versioned::V7ToV8<Runtime>,
);
}

Expand Down
276 changes: 149 additions & 127 deletions polkadot/runtime/westend/src/weights/pallet_nomination_pools.rs

Large diffs are not rendered by default.

41 changes: 35 additions & 6 deletions substrate/frame/nomination-pools/benchmarking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ use frame_support::{
use frame_system::RawOrigin as RuntimeOrigin;
use pallet_nomination_pools::{
BalanceOf, BondExtra, BondedPoolInner, BondedPools, ClaimPermission, ClaimPermissions,
Commission, CommissionChangeRate, ConfigOp, GlobalMaxCommission, MaxPoolMembers,
MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond, Pallet as Pools,
PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage,
Commission, CommissionChangeRate, CommissionClaimPermission, ConfigOp, GlobalMaxCommission,
MaxPoolMembers, MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond,
Pallet as Pools, PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage,
};
use pallet_staking::MaxNominationsOf;
use sp_runtime::{
Expand Down Expand Up @@ -706,17 +706,24 @@ frame_benchmarking::benchmarks! {
max_increase: Perbill::from_percent(20),
min_delay: 0u32.into(),
}).unwrap();
// set a claim permission to an account.
Pools::<T>::set_commission_claim_permission(
RuntimeOrigin::Signed(depositor.clone()).into(),
1u32.into(),
Some(CommissionClaimPermission::Account(depositor.clone()))
).unwrap();

}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Some((Perbill::from_percent(20), depositor.clone())))
verify {
assert_eq!(BondedPools::<T>::get(1).unwrap().commission, Commission {
current: Some((Perbill::from_percent(20), depositor)),
current: Some((Perbill::from_percent(20), depositor.clone())),
max: Some(Perbill::from_percent(50)),
change_rate: Some(CommissionChangeRate {
max_increase: Perbill::from_percent(20),
min_delay: 0u32.into()
}),
throttle_from: Some(1u32.into()),
claim_permission: Some(CommissionClaimPermission::Account(depositor)),
});
}

Expand All @@ -731,6 +738,7 @@ frame_benchmarking::benchmarks! {
max: Some(Perbill::from_percent(50)),
change_rate: None,
throttle_from: Some(0u32.into()),
claim_permission: None,
});
}

Expand All @@ -751,9 +759,25 @@ frame_benchmarking::benchmarks! {
min_delay: 1000u32.into(),
}),
throttle_from: Some(1_u32.into()),
claim_permission: None,
});
}

set_commission_claim_permission {
// Create a pool.
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Some(CommissionClaimPermission::Account(depositor.clone())))
verify {
assert_eq!(
BondedPools::<T>::get(1).unwrap().commission, Commission {
current: None,
max: None,
change_rate: None,
throttle_from: None,
claim_permission: Some(CommissionClaimPermission::Account(depositor)),
});
}

set_claim_permission {
// Create a pool
let min_create_bond = Pools::<T>::depositor_min_bond();
Expand Down Expand Up @@ -786,8 +810,13 @@ frame_benchmarking::benchmarks! {
CurrencyOf::<T>::set_balance(&reward_account, ed + origin_weight);

// 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.clone()).into());
// set a claim permission to an account.
let _ = Pools::<T>::set_commission_claim_permission(
RuntimeOrigin::Signed(depositor.clone()).into(),
1u32.into(),
Some(CommissionClaimPermission::Account(claimer))
);
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into())
verify {
Expand Down
58 changes: 55 additions & 3 deletions substrate/frame/nomination-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,13 @@ pub struct PoolRoles<AccountId> {
pub bouncer: Option<AccountId>,
}

// A pool's possible commission claiming permissions.
#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum CommissionClaimPermission<AccountId> {
Permissionless,
Account(AccountId),
}

/// Pool commission.
///
/// The pool `root` can set commission configuration after pool creation. By default, all commission
Expand Down Expand Up @@ -705,6 +712,9 @@ pub struct Commission<T: Config> {
/// The block from where throttling should be checked from. This value will be updated on all
/// commission updates and when setting an initial `change_rate`.
pub throttle_from: Option<BlockNumberFor<T>>,
// Whether commission can be claimed permissionlessly, or whether an account can claim
// commission. `Root` role can always claim.
pub claim_permission: Option<CommissionClaimPermission<T::AccountId>>,
}

impl<T: Config> Commission<T> {
Expand Down Expand Up @@ -1078,6 +1088,17 @@ impl<T: Config> BondedPool<T> {
self.is_root(who)
}

fn can_claim_commission(&self, who: &T::AccountId) -> bool {
if let Some(permission) = self.commission.claim_permission.as_ref() {
match permission {
CommissionClaimPermission::Permissionless => true,
CommissionClaimPermission::Account(account) => account == who || self.is_root(who),
}
} else {
self.is_root(who)
}
}

fn is_destroying(&self) -> bool {
matches!(self.state, PoolState::Destroying)
}
Expand Down Expand Up @@ -1572,7 +1593,7 @@ pub mod pallet {
use sp_runtime::Perbill;

/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(7);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(8);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -1850,6 +1871,11 @@ pub mod pallet {
pool_id: PoolId,
change_rate: CommissionChangeRate<BlockNumberFor<T>>,
},
/// Pool commission claim permission has been updated.
PoolCommissionClaimPermissionUpdated {
pool_id: PoolId,
permission: Option<CommissionClaimPermission<T::AccountId>>,
},
/// Pool commission has been claimed.
PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf<T> },
/// Topped up deficit in frozen ED of the reward pool.
Expand Down Expand Up @@ -2742,6 +2768,32 @@ pub mod pallet {
let who = ensure_signed(origin)?;
Self::do_adjust_pool_deposit(who, pool_id)
}

/// Set or remove a pool's commission claim permission.
///
/// Determines who can claim the pool's pending commission. Only the `Root` role of the pool
/// is able to conifigure commission claim permissions.
#[pallet::call_index(22)]
#[pallet::weight(T::WeightInfo::set_commission_claim_permission())]
pub fn set_commission_claim_permission(
origin: OriginFor<T>,
pool_id: PoolId,
permission: Option<CommissionClaimPermission<T::AccountId>>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_manage_commission(&who), Error::<T>::DoesNotHavePermission);

bonded_pool.commission.claim_permission = permission.clone();
bonded_pool.put();

Self::deposit_event(Event::<T>::PoolCommissionClaimPermissionUpdated {
pool_id,
permission,
});

Ok(())
}
}

#[pallet::hooks]
Expand Down Expand Up @@ -3106,12 +3158,12 @@ impl<T: Config> Pallet<T> {

fn do_claim_commission(who: T::AccountId, pool_id: PoolId) -> DispatchResult {
let bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_manage_commission(&who), Error::<T>::DoesNotHavePermission);
ensure!(bonded_pool.can_claim_commission(&who), Error::<T>::DoesNotHavePermission);

let mut reward_pool = RewardPools::<T>::get(pool_id)
.defensive_ok_or::<Error<T>>(DefensiveError::RewardPoolNotFound.into())?;

// IMPORTANT: make sure that any newly pending commission not yet processed is added to
// IMPORTANT: ensure newly pending commission not yet processed is added to
// `total_commission_pending`.
reward_pool.update_records(
pool_id,
Expand Down
77 changes: 77 additions & 0 deletions substrate/frame/nomination-pools/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ use sp_runtime::TryRuntimeError;
pub mod versioned {
use super::*;

/// v8: Adds commission claim permissions to `BondedPools`.
pub type V7ToV8<T> = frame_support::migrations::VersionedMigration<
7,
8,
v8::VersionUncheckedMigrateV7ToV8<T>,
crate::pallet::Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;

/// Migration V6 to V7 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring
/// the migration is only performed when on-chain version is 6.
pub type V6ToV7<T> = frame_support::migrations::VersionedMigration<
Expand All @@ -47,6 +56,74 @@ pub mod versioned {
>;
}

pub mod v8 {
use super::*;

#[derive(Decode)]
pub struct OldCommission<T: Config> {
pub current: Option<(Perbill, T::AccountId)>,
pub max: Option<Perbill>,
pub change_rate: Option<CommissionChangeRate<BlockNumberFor<T>>>,
pub throttle_from: Option<BlockNumberFor<T>>,
}

#[derive(Decode)]
pub struct OldBondedPoolInner<T: Config> {
pub commission: OldCommission<T>,
pub member_counter: u32,
pub points: BalanceOf<T>,
pub roles: PoolRoles<T::AccountId>,
pub state: PoolState,
}

impl<T: Config> OldBondedPoolInner<T> {
fn migrate_to_v8(self) -> BondedPoolInner<T> {
BondedPoolInner {
commission: Commission {
current: self.commission.current,
max: self.commission.max,
change_rate: self.commission.change_rate,
throttle_from: self.commission.throttle_from,
// `claim_permission` is a new field.
claim_permission: None,
},
member_counter: self.member_counter,
points: self.points,
roles: self.roles,
state: self.state,
}
}
}

pub struct VersionUncheckedMigrateV7ToV8<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for VersionUncheckedMigrateV7ToV8<T> {
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
Ok(Vec::new())
}

fn on_runtime_upgrade() -> Weight {
let mut translated = 0u64;
BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
translated.saturating_inc();
Some(old_value.migrate_to_v8())
});
T::DbWeight::get().reads_writes(translated, translated + 1)
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
// Check new `claim_permission` field is present.
ensure!(
BondedPools::<T>::iter()
.all(|(_, inner)| inner.commission.claim_permission.is_none()),
"`claim_permission` value has not been set correctly."
);
Ok(())
}
}
}

/// This migration accumulates and initializes the [`TotalValueLocked`] for all pools.
///
/// WARNING: This migration works under the assumption that the [`BondedPools`] cannot be inflated
Expand Down
Loading

0 comments on commit 7506271

Please sign in to comment.