diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index d4a72ebb9c45a..ce9dc53043b47 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -566,6 +566,7 @@ impl pallet_staking::Config for Runtime { type OnStakerSlash = NominationPools; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = StakingBenchmarkingConfig; + type BondingRestriction = (); } parameter_types! { diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 5677eb7e28e49..61730c478cbc7 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -206,6 +206,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type BondingRestriction = (); } impl pallet_offences::Config for Test { diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index 5e6c955c441c5..54d968ae32bdd 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -210,6 +210,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type BondingRestriction = (); } impl pallet_offences::Config for Test { diff --git a/frame/nomination-pools/benchmarking/src/mock.rs b/frame/nomination-pools/benchmarking/src/mock.rs index eb884869f6d32..a441a7eb6f5bf 100644 --- a/frame/nomination-pools/benchmarking/src/mock.rs +++ b/frame/nomination-pools/benchmarking/src/mock.rs @@ -113,6 +113,7 @@ impl pallet_staking::Config for Runtime { type OnStakerSlash = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type BondingRestriction = (); } parameter_types! { diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index d51a81b1212c0..a6239799f0900 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -182,6 +182,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type BondingRestriction = (); } impl pallet_im_online::Config for Test { diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index c777f2c56de3a..5b25b31bd14b2 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -188,6 +188,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type BondingRestriction = (); } impl crate::Config for Test {} diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 331095774b741..611805b796613 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -917,3 +917,19 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { type MaxValidators = frame_support::traits::ConstU32<100>; type MaxNominators = frame_support::traits::ConstU32<100>; } + +/// Means for checking if there is any external restriction on bonding with a specific account +/// +/// Allows for parts of the runtime that might implement other forms of fund locking to prevent +/// incompatible locking on a stash account which could lead to unsafe state. +pub trait BondingRestriction { + /// Determine if bonding is allowed with stash and controller combination + fn can_bond(stash: &AccountId) -> bool; +} + +// No restrictions +impl BondingRestriction for () { + fn can_bond(_stash: &AccountId) -> bool { + true + } +} diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index bd2d8cdc32ce9..4c2cdde61c12a 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -41,6 +41,7 @@ use std::cell::RefCell; pub const INIT_TIMESTAMP: u64 = 30_000; pub const BLOCK_TIME: u64 = 1000; +pub const RESTRICTED_ACCOUNT: AccountId = 55500; /// The AccountId alias in this test module. pub(crate) type AccountId = u64; @@ -302,6 +303,7 @@ impl crate::pallet::pallet::Config for Test { type OnStakerSlash = OnStakerSlashMock; type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); + type BondingRestriction = AccountRestricted555; } impl frame_system::offchain::SendTransactionTypes for Test @@ -887,3 +889,12 @@ pub(crate) fn staking_events() -> Vec> { pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) { (Balances::free_balance(who), Balances::reserved_balance(who)) } + +pub struct AccountRestricted555; + +// Restrict stash account +impl BondingRestriction for AccountRestricted555 { + fn can_bond(stash: &AccountId) -> bool { + *stash != RESTRICTED_ACCOUNT + } +} diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index e53464195de23..9656d26bad34c 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -40,10 +40,10 @@ mod impls; pub use impls::*; use crate::{ - slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraPayout, EraRewardPoints, Exposure, - Forcing, MaxUnlockingChunks, NegativeImbalanceOf, Nominations, PositiveImbalanceOf, Releases, - RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, - ValidatorPrefs, + slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, BondingRestriction, EraPayout, + EraRewardPoints, Exposure, Forcing, MaxUnlockingChunks, NegativeImbalanceOf, Nominations, + PositiveImbalanceOf, Releases, RewardDestination, SessionInterface, StakingLedger, + UnappliedSlash, UnlockChunk, ValidatorPrefs, }; const STAKING_ID: LockIdentifier = *b"staking "; @@ -200,6 +200,9 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + /// Interface for doing bonding restriction check + type BondingRestriction: BondingRestriction; } #[pallet::type_value] @@ -705,6 +708,8 @@ pub mod pallet { TooManyValidators, /// Commission is too low. Must be at least `MinCommission`. CommissionTooLow, + /// External restriction prevents bonding with given account + BondingRestricted, } #[pallet::hooks] @@ -788,6 +793,10 @@ pub mod pallet { return Err(Error::::AlreadyPaired.into()) } + if !T::BondingRestriction::can_bond(&stash) { + return Err(Error::::BondingRestricted.into()) + } + // Reject a bond which is considered to be _dust_. if value < T::Currency::minimum_balance() { return Err(Error::::InsufficientBond.into()) diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index ccd9558c5c21d..b16afb9416da5 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -20,7 +20,7 @@ use super::{ConfigOp, Event, MaxUnlockingChunks, *}; use frame_election_provider_support::{ElectionProvider, SortedListProvider, Support}; use frame_support::{ - assert_noop, assert_ok, assert_storage_noop, bounded_vec, + assert_err, assert_noop, assert_ok, assert_storage_noop, bounded_vec, dispatch::WithPostDispatchInfo, pallet_prelude::*, traits::{Currency, Get, ReservableCurrency}, @@ -5141,3 +5141,30 @@ fn proportional_ledger_slash_works() { BTreeMap::from([(4, 0), (5, value_slashed), (6, 0), (7, 0)]) ); } + +#[test] +fn bond_restriction_works() { + ExtBuilder::default().build_and_execute(|| { + start_session(2); + assert_err!( + Staking::bond( + Origin::signed(RESTRICTED_ACCOUNT), + 1, + 1000, + RewardDestination::Controller + ), + Error::::BondingRestricted + ); + + // put some money in account that we'll use. + let _ = Balances::make_free_balance_be(&(RESTRICTED_ACCOUNT + 1), 2000); + + // Using unrestricted account should not fail + assert_ok!(Staking::bond( + Origin::signed(RESTRICTED_ACCOUNT + 1), + 1, + 1000, + RewardDestination::Controller + )); + }); +}