Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Nomination Pool] Make staking restrictions configurable #7685

Merged
merged 14 commits into from
Feb 26, 2025
1 change: 1 addition & 0 deletions polkadot/runtime/test-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ impl pallet_staking::Config for Runtime {
type MaxValidatorSet = MaxAuthorities;
type MaxInvulnerables = ConstU32<20>;
type MaxDisabledValidators = ConstU32<100>;
type Filter = frame_support::traits::Nothing;
}

parameter_types! {
Expand Down
4 changes: 3 additions & 1 deletion polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use frame_support::{
traits::{
fungible::HoldConsideration, tokens::UnityOrOuterConversion, ConstU32, Contains, EitherOf,
EitherOfDiverse, EnsureOriginWithArg, EverythingBut, FromContains, InstanceFilter,
KeyOwnerProofSystem, LinearStoragePrice, ProcessMessage, ProcessMessageError,
KeyOwnerProofSystem, LinearStoragePrice, Nothing, ProcessMessage, ProcessMessageError,
VariantCountOf, WithdrawReasons,
},
weights::{ConstantMultiplier, WeightMeter, WeightToFee as _},
Expand Down Expand Up @@ -772,6 +772,7 @@ impl pallet_staking::Config for Runtime {
type WeightInfo = weights::pallet_staking::WeightInfo<Runtime>;
type MaxInvulnerables = frame_support::traits::ConstU32<20>;
type MaxDisabledValidators = ConstU32<100>;
type Filter = Nothing;
}

impl pallet_fast_unstake::Config for Runtime {
Expand Down Expand Up @@ -1547,6 +1548,7 @@ impl pallet_nomination_pools::Config for Runtime {
type MaxPointsToBalance = MaxPointsToBalance;
type AdminOrigin = EitherOf<EnsureRoot<AccountId>, StakingAdmin>;
type BlockNumberProvider = System;
type Filter = Nothing;
}

parameter_types! {
Expand Down
20 changes: 20 additions & 0 deletions prdoc/pr_7685.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
title: 'Introduce filters to restrict accounts from staking'

doc:
- audience: Runtime Dev
description: |
Introduce filters to restrict accounts from staking.
This is useful for restricting certain accounts from staking, for example, accounts staking via pools, and vice
versa.

crates:
- name: pallet-staking
bump: minor
- name: pallet-nomination-pools
bump: minor
- name: westend-runtime
bump: patch
- name: pallet-delegated-staking
bump: patch
- name: pallet-nomination-pools-benchmarking
bump: patch
2 changes: 2 additions & 0 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ impl pallet_staking::Config for Runtime {
type BenchmarkingConfig = StakingBenchmarkingConfig;
type MaxInvulnerables = ConstU32<20>;
type MaxDisabledValidators = ConstU32<100>;
type Filter = Nothing;
}

impl pallet_fast_unstake::Config for Runtime {
Expand Down Expand Up @@ -1236,6 +1237,7 @@ impl pallet_nomination_pools::Config for Runtime {
pallet_collective::EnsureProportionAtLeast<AccountId, CouncilCollective, 3, 4>,
>;
type BlockNumberProvider = System;
type Filter = Nothing;
}

parameter_types! {
Expand Down
10 changes: 0 additions & 10 deletions substrate/frame/delegated-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,6 @@ pub mod pallet {
// Existing `agent` cannot register again and a delegator cannot become an `agent`.
ensure!(!Self::is_agent(&who) && !Self::is_delegator(&who), Error::<T>::NotAllowed);

// They cannot be already a direct staker in the staking pallet.
ensure!(!Self::is_direct_staker(&who), Error::<T>::AlreadyStaking);

// Reward account cannot be same as `agent` account.
ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);

Expand Down Expand Up @@ -407,7 +404,6 @@ pub mod pallet {
// Ensure delegator is sane.
ensure!(!Self::is_agent(&delegator), Error::<T>::NotAllowed);
ensure!(!Self::is_delegator(&delegator), Error::<T>::NotAllowed);
ensure!(!Self::is_direct_staker(&delegator), Error::<T>::AlreadyStaking);

// ensure agent is sane.
ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
Expand Down Expand Up @@ -442,12 +438,6 @@ pub mod pallet {
Error::<T>::InvalidDelegation
);

// Implementation note: Staking uses deprecated locks (similar to freeze) which are not
// mutually exclusive of holds. This means, if we allow delegating for existing stakers,
// already staked funds might be reused for delegation. We avoid that by just blocking
// this.
ensure!(!Self::is_direct_staker(&delegator), Error::<T>::AlreadyStaking);

// ensure agent is sane.
ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);

Expand Down
2 changes: 2 additions & 0 deletions substrate/frame/delegated-staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ impl pallet_staking::Config for Runtime {
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>;
type EventListeners = (Pools, DelegatedStaking);
type Filter = pallet_nomination_pools::AllPoolMembers<Self>;
}

parameter_types! {
Expand Down Expand Up @@ -164,6 +165,7 @@ impl pallet_nomination_pools::Config for Runtime {
pallet_nomination_pools::adapter::DelegateStake<Self, Staking, DelegatedStaking>;
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type BlockNumberProvider = System;
type Filter = pallet_staking::AllStakers<Runtime>;
}

frame_support::construct_runtime!(
Expand Down
83 changes: 30 additions & 53 deletions substrate/frame/delegated-staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,6 @@ fn cannot_become_agent() {
DelegatedStaking::register_agent(RawOrigin::Signed(100).into(), 100),
Error::<T>::InvalidRewardDestination
);

// an existing validator cannot become agent
assert_noop!(
DelegatedStaking::register_agent(
RawOrigin::Signed(mock::GENESIS_VALIDATOR).into(),
100
),
Error::<T>::AlreadyStaking
);

// an existing direct staker to `CoreStaking` cannot become an agent.
assert_noop!(
DelegatedStaking::register_agent(
RawOrigin::Signed(mock::GENESIS_NOMINATOR_ONE).into(),
100
),
Error::<T>::AlreadyStaking
);
assert_noop!(
DelegatedStaking::register_agent(
RawOrigin::Signed(mock::GENESIS_NOMINATOR_TWO).into(),
100
),
Error::<T>::AlreadyStaking
);
});
}

Expand Down Expand Up @@ -637,18 +612,6 @@ mod staking_integration {
DelegatedStaking::register_agent(RawOrigin::Signed(202).into(), 203),
Error::<T>::NotAllowed
);
// existing staker cannot become a delegate
assert_noop!(
DelegatedStaking::register_agent(
RawOrigin::Signed(GENESIS_NOMINATOR_ONE).into(),
201
),
Error::<T>::AlreadyStaking
);
assert_noop!(
DelegatedStaking::register_agent(RawOrigin::Signed(GENESIS_VALIDATOR).into(), 201),
Error::<T>::AlreadyStaking
);
});
}

Expand Down Expand Up @@ -1362,7 +1325,7 @@ mod pool_integration {
}

#[test]
fn existing_pool_member_can_stake() {
fn existing_pool_member_cannot_stake() {
// A pool member is able to stake directly since staking only uses free funds but once a
// staker, they cannot join/add extra bond to the pool. They can still withdraw funds.
ExtBuilder::default().build_and_execute(|| {
Expand All @@ -1376,28 +1339,42 @@ mod pool_integration {
fund(&delegator, 1000);
assert_ok!(Pools::join(RawOrigin::Signed(delegator).into(), 200, pool_id));

// THEN: they can still stake directly.
// THEN: they cannot stake anymore
assert_noop!(
Staking::bond(
RuntimeOrigin::signed(delegator),
500,
RewardDestination::Account(101)
),
StakingError::<T>::Restricted
);
});
}

#[test]
fn stakers_cannot_join_pool() {
ExtBuilder::default().build_and_execute(|| {
start_era(1);
// GIVEN: a pool.
fund(&200, 1000);
let pool_id = create_pool(200, 800);

// WHEN: an account is a staker.
let staker = 100;
fund(&staker, 1000);

assert_ok!(Staking::bond(
RuntimeOrigin::signed(delegator),
RuntimeOrigin::signed(staker),
500,
RewardDestination::Account(101)
));
assert_ok!(Staking::nominate(
RuntimeOrigin::signed(delegator),
vec![GENESIS_VALIDATOR]
));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(staker), vec![GENESIS_VALIDATOR]));

// The delegator cannot add any extra bond to the pool anymore.
// THEN: they cannot join pool.
assert_noop!(
Pools::bond_extra(RawOrigin::Signed(delegator).into(), BondExtra::FreeBalance(100)),
Error::<T>::AlreadyStaking
Pools::join(RawOrigin::Signed(staker).into(), 200, pool_id),
PoolsError::<T>::Restricted
);

// But they can unbond
assert_ok!(Pools::unbond(RawOrigin::Signed(delegator).into(), delegator, 50));
// and withdraw
start_era(4);
assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(delegator).into(), delegator, 0));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ use pallet_staking::{ActiveEra, CurrentEra, ErasStartSessionIndex, StakerStatus}
use parking_lot::RwLock;
use std::sync::Arc;

use frame_support::derive_impl;

use crate::{log, log_current_time};
use frame_support::{derive_impl, traits::Nothing};

pub const INIT_TIMESTAMP: BlockNumber = 30_000;
pub const BLOCK_TIME: BlockNumber = 1000;
Expand Down Expand Up @@ -290,6 +289,7 @@ impl pallet_nomination_pools::Config for Runtime {
type MaxPointsToBalance = frame_support::traits::ConstU8<10>;
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type BlockNumberProvider = System;
type Filter = Nothing;
}

parameter_types! {
Expand Down
3 changes: 2 additions & 1 deletion substrate/frame/nomination-pools/benchmarking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use frame_support::{
derive_impl,
pallet_prelude::*,
parameter_types,
traits::{ConstU64, VariantCountOf},
traits::{ConstU64, Nothing, VariantCountOf},
PalletId,
};
use sp_runtime::{
Expand Down Expand Up @@ -141,6 +141,7 @@ impl pallet_nomination_pools::Config for Runtime {
type MaxPointsToBalance = MaxPointsToBalance;
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type BlockNumberProvider = System;
type Filter = Nothing;
}

parameter_types! {
Expand Down
34 changes: 31 additions & 3 deletions substrate/frame/nomination-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ use frame_support::{
traits::{
fungible::{Inspect, InspectFreeze, Mutate, MutateFreeze},
tokens::{Fortitude, Preservation},
Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get,
Contains, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get,
},
DefaultNoBound, PalletError,
};
Expand Down Expand Up @@ -1729,6 +1729,9 @@ pub mod pallet {

/// Provider for the block number. Normally this is the `frame_system` pallet.
type BlockNumberProvider: BlockNumberProvider;

/// Restrict some accounts from participating in a nomination pool.
type Filter: Contains<Self::AccountId>;
}

/// The sum of funds across all pools.
Expand Down Expand Up @@ -2049,6 +2052,9 @@ pub mod pallet {
NotMigrated,
/// This call is not allowed in the current state of the pallet.
NotSupported,
/// Account is restricted from participation in pools. This may happen if the account is
/// staking in another way already.
Restricted,
}

#[derive(
Expand Down Expand Up @@ -2088,8 +2094,9 @@ pub mod pallet {

#[pallet::call]
impl<T: Config> Pallet<T> {
/// Stake funds with a pool. The amount to bond is transferred from the member to the pool
/// account and immediately increases the pools bond.
/// Stake funds with a pool. The amount to bond is delegated (or transferred based on
/// [`adapter::StakeStrategyType`]) from the member to the pool account and immediately
/// increases the pool's bond.
///
/// The method of transferring the amount to the pool account is determined by
/// [`adapter::StakeStrategyType`]. If the pool is configured to use
Expand All @@ -2114,6 +2121,9 @@ pub mod pallet {
// ensure pool is not in an un-migrated state.
ensure!(!Self::api_pool_needs_delegate_migration(pool_id), Error::<T>::NotMigrated);

// ensure account is not restricted from joining the pool.
ensure!(!T::Filter::contains(&who), Error::<T>::Restricted);

ensure!(amount >= MinJoinBond::<T>::get(), Error::<T>::MinimumBondNotMet);
// If a member already exists that means they already belong to a pool
ensure!(!PoolMembers::<T>::contains_key(&who), Error::<T>::AccountBelongsToOtherPool);
Expand Down Expand Up @@ -3148,12 +3158,16 @@ pub mod pallet {
) -> DispatchResultWithPostInfo {
let _caller = ensure_signed(origin)?;

// ensure `DelegateStake` strategy is used.
ensure!(
T::StakeAdapter::strategy_type() == adapter::StakeStrategyType::Delegate,
Error::<T>::NotSupported
);

// ensure member is not restricted from joining the pool.
let member_account = T::Lookup::lookup(member_account)?;
ensure!(!T::Filter::contains(&member_account), Error::<T>::Restricted);

let member =
PoolMembers::<T>::get(&member_account).ok_or(Error::<T>::PoolMemberNotFound)?;

Expand Down Expand Up @@ -3482,6 +3496,9 @@ impl<T: Config> Pallet<T> {
bouncer: AccountIdLookupOf<T>,
pool_id: PoolId,
) -> DispatchResult {
// ensure depositor is not restricted from joining the pool.
ensure!(!T::Filter::contains(&who), Error::<T>::Restricted);

let root = T::Lookup::lookup(root)?;
let nominator = T::Lookup::lookup(nominator)?;
let bouncer = T::Lookup::lookup(bouncer)?;
Expand Down Expand Up @@ -3555,6 +3572,9 @@ impl<T: Config> Pallet<T> {
member_account: T::AccountId,
extra: BondExtra<BalanceOf<T>>,
) -> DispatchResult {
// ensure account is not restricted from joining the pool.
ensure!(!T::Filter::contains(&member_account), Error::<T>::Restricted);

if signer != member_account {
ensure!(
ClaimPermissions::<T>::get(&member_account).can_bond_extra(),
Expand Down Expand Up @@ -4224,3 +4244,11 @@ impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pall
}
}
}

/// A utility struct that provides a way to check if a given account is a pool member.
pub struct AllPoolMembers<T: Config>(PhantomData<T>);
impl<T: Config> Contains<T::AccountId> for AllPoolMembers<T> {
fn contains(t: &T::AccountId) -> bool {
PoolMembers::<T>::contains_key(t)
}
}
Loading
Loading