Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,22 @@ sp_api::impl_runtime_apis! {
fn balance_to_points(pool_id: pallet_nomination_pools::PoolId, new_funds: Balance) -> Balance {
NominationPools::api_balance_to_points(pool_id, new_funds)
}

fn pool_pending_slash(pool_id: pallet_nomination_pools::PoolId) -> Balance {
NominationPools::api_pool_pending_slash(pool_id)
}

fn member_pending_slash(member: AccountId) -> Balance {
NominationPools::api_member_pending_slash(member)
}

fn pool_needs_delegate_migration(pool_id: pallet_nomination_pools::PoolId) -> bool {
NominationPools::api_pool_needs_delegate_migration(pool_id)
}

fn member_needs_delegate_migration(member: AccountId) -> bool {
NominationPools::api_member_needs_delegate_migration(member)
}
}

impl pallet_staking_runtime_api::StakingApi<Block, Balance, AccountId> for Runtime {
Expand Down
21 changes: 21 additions & 0 deletions prdoc/pr_4647.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json

title: Runtime apis to help with delegate-stake based Nomination Pools.

doc:
- audience: Runtime User
description: |
Introduces a new set of runtime apis to facilitate dapps and wallets to integrate with delegate-stake
functionalities of Nomination Pools. These apis support pool and member migration, as well as lazy application of
pending slashes of the pool members.

crates:
- name: pallet-nomination-pools
bump: minor
- name: westend-runtime
bump: minor
- name: kitchensink-runtime
bump: minor
- name: pallet-delegated-staking
bump: patch
16 changes: 16 additions & 0 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,22 @@ impl_runtime_apis! {
fn balance_to_points(pool_id: pallet_nomination_pools::PoolId, new_funds: Balance) -> Balance {
NominationPools::api_balance_to_points(pool_id, new_funds)
}

fn pool_pending_slash(pool_id: pallet_nomination_pools::PoolId) -> Balance {
NominationPools::api_pool_pending_slash(pool_id)
}

fn member_pending_slash(member: AccountId) -> Balance {
NominationPools::api_member_pending_slash(member)
}

fn pool_needs_delegate_migration(pool_id: pallet_nomination_pools::PoolId) -> bool {
NominationPools::api_pool_needs_delegate_migration(pool_id)
}

fn member_needs_delegate_migration(member: AccountId) -> bool {
NominationPools::api_member_needs_delegate_migration(member)
}
}

impl pallet_staking_runtime_api::StakingApi<Block, Balance, AccountId> for Runtime {
Expand Down
8 changes: 3 additions & 5 deletions substrate/frame/delegated-staking/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,9 @@ impl<T: Config> DelegationInterface for Pallet<T> {
)
}

/// Returns true if the `Agent` have any slash pending to be applied.
fn has_pending_slash(agent: &Self::AccountId) -> bool {
Agent::<T>::get(agent)
.map(|d| !d.ledger.pending_slash.is_zero())
.unwrap_or(false)
/// Returns pending slash of the `agent`.
fn pending_slash(agent: &Self::AccountId) -> Option<Self::Balance> {
Agent::<T>::get(agent).map(|d| d.ledger.pending_slash).ok()
}

fn delegator_slash(
Expand Down
27 changes: 27 additions & 0 deletions substrate/frame/delegated-staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,11 @@ mod pool_integration {
BondedPools::<T>::get(1).unwrap().points,
creator_stake + delegator_stake * 6 - delegator_stake * 3
);

// pool has currently no pending slash
assert_eq!(Pools::api_pool_pending_slash(pool_id), 0);

// slash the pool partially
pallet_staking::slashing::do_slash::<T>(
&pool_acc,
500,
Expand All @@ -1077,6 +1082,9 @@ mod pool_integration {
3,
);

// pool has now pending slash of 500.
assert_eq!(Pools::api_pool_pending_slash(pool_id), 500);

assert_eq!(
pool_events_since_last_call(),
vec![
Expand Down Expand Up @@ -1139,19 +1147,38 @@ mod pool_integration {

for i in 303..306 {
let pre_pending_slash = get_pool_agent(pool_id).ledger.pending_slash;
// pool api returns correct pending slash.
assert_eq!(Pools::api_pool_pending_slash(pool_id), pre_pending_slash);
// delegator has pending slash of 50.
assert_eq!(Pools::api_member_pending_slash(i), 50);
// apply slash
assert_ok!(Pools::apply_slash(RawOrigin::Signed(slash_reporter).into(), i));
// nothing pending anymore.
assert_eq!(Pools::api_member_pending_slash(i), 0);

// each member is slashed 50% of 100 = 50.
assert_eq!(get_pool_agent(pool_id).ledger.pending_slash, pre_pending_slash - 50);
// pool api returns correctly as well.
assert_eq!(Pools::api_pool_pending_slash(pool_id), pre_pending_slash - 50);
// left with 50.
assert_eq!(DelegatedStaking::held_balance_of(&i), 50);
}

// pool has still pending slash of creator.
assert_eq!(Pools::api_pool_pending_slash(pool_id), 250);

// reporter is paid SlashRewardFraction of the slash, i.e. 10% of 50 = 5
assert_eq!(Balances::free_balance(slash_reporter), 100 + 5 * 3);
// creator has pending slash.
assert_eq!(Pools::api_member_pending_slash(creator), 250);
// slash creator
assert_ok!(Pools::apply_slash(RawOrigin::Signed(slash_reporter).into(), creator));
// no pending slash anymore.
assert_eq!(Pools::api_member_pending_slash(creator), 0);

// all slash should be applied now.
assert_eq!(get_pool_agent(pool_id).ledger.pending_slash, 0);
assert_eq!(Pools::api_pool_pending_slash(pool_id), 0);
// for creator, 50% of stake should be slashed (250), 10% of which should go to reporter
// (25).
assert_eq!(Balances::free_balance(slash_reporter), 115 + 25);
Expand Down
25 changes: 25 additions & 0 deletions substrate/frame/nomination-pools/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,30 @@ sp_api::decl_runtime_apis! {

/// Returns the equivalent points of `new_funds` for a given pool.
fn balance_to_points(pool_id: PoolId, new_funds: Balance) -> Balance;

/// Returns the pending slash for a given pool.
fn pool_pending_slash(pool_id: PoolId) -> Balance;

/// Returns the pending slash for a given pool member.
fn member_pending_slash(member: AccountId) -> Balance;

/// Returns true if the pool with `pool_id` needs migration.
///
/// This can happen when the `pallet-nomination-pools` has switched to using strategy
/// [`DelegateStake`](pallet_nomination_pools::adapter::DelegateStake) but the pool
/// still has funds that were staked using the older strategy
/// [TransferStake](pallet_nomination_pools::adapter::TransferStake). Use
/// [`migrate_pool_to_delegate_stake`](pallet_nomination_pools::Call::migrate_pool_to_delegate_stake)
/// to migrate the pool.
fn pool_needs_delegate_migration(pool_id: PoolId) -> bool;

/// Returns true if the delegated funds of the pool `member` needs migration.
///
/// Once a pool has successfully migrated to the strategy
/// [`DelegateStake`](pallet_nomination_pools::adapter::DelegateStake), the funds of the
/// member can be migrated from pool account to the member's account. Use
/// [`migrate_delegation`](pallet_nomination_pools::Call::migrate_delegation)
/// to migrate the funds of the pool member.
fn member_needs_delegate_migration(member: AccountId) -> bool;
}
}
10 changes: 5 additions & 5 deletions substrate/frame/nomination-pools/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ pub trait StakeStrategy {
) -> DispatchResult;

/// Check if there is any pending slash for the pool.
fn has_pending_slash(pool_account: &Self::AccountId) -> bool;
fn pending_slash(pool_account: &Self::AccountId) -> Self::Balance;

/// Slash the member account with `amount` against pending slashes for the pool.
fn member_slash(
Expand Down Expand Up @@ -254,9 +254,9 @@ impl<T: Config, Staking: StakingInterface<Balance = BalanceOf<T>, AccountId = T:
Ok(())
}

fn has_pending_slash(_: &Self::AccountId) -> bool {
fn pending_slash(_: &Self::AccountId) -> Self::Balance {
// for transfer stake strategy, slashing is greedy and never deferred.
false
Zero::zero()
}

fn member_slash(
Expand Down Expand Up @@ -354,8 +354,8 @@ impl<
Delegation::withdraw_delegation(&who, pool_account, amount, num_slashing_spans)
}

fn has_pending_slash(pool_account: &Self::AccountId) -> bool {
Delegation::has_pending_slash(pool_account)
fn pending_slash(pool_account: &Self::AccountId) -> Self::Balance {
Delegation::pending_slash(pool_account).defensive_unwrap_or_default()
}

fn member_slash(
Expand Down
110 changes: 94 additions & 16 deletions substrate/frame/nomination-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3468,31 +3468,53 @@ impl<T: Config> Pallet<T> {
member_account: &T::AccountId,
reporter: Option<T::AccountId>,
) -> DispatchResult {
// calculate points to be slashed.
let member =
PoolMembers::<T>::get(&member_account).ok_or(Error::<T>::PoolMemberNotFound)?;
let member = PoolMembers::<T>::get(member_account).ok_or(Error::<T>::PoolMemberNotFound)?;

let pool_account = Pallet::<T>::generate_bonded_account(member.pool_id);
ensure!(T::StakeAdapter::has_pending_slash(&pool_account), Error::<T>::NothingToSlash);

let unslashed_balance = T::StakeAdapter::member_delegation_balance(&member_account);
let slashed_balance = member.total_balance();
defensive_assert!(
unslashed_balance >= slashed_balance,
"unslashed balance should always be greater or equal to the slashed"
);
let pending_slash = Self::member_pending_slash(member_account, member.clone());

// if nothing to slash, return error.
ensure!(unslashed_balance > slashed_balance, Error::<T>::NothingToSlash);
ensure!(!pending_slash.is_zero(), Error::<T>::NothingToSlash);

T::StakeAdapter::member_slash(
&member_account,
&pool_account,
unslashed_balance.defensive_saturating_sub(slashed_balance),
member_account,
&Pallet::<T>::generate_bonded_account(member.pool_id),
pending_slash,
reporter,
)
}

/// Pending slash for a member.
///
/// Takes the pool_member object corresponding to the `member_account`.
fn member_pending_slash(
member_account: &T::AccountId,
pool_member: PoolMember<T>,
) -> BalanceOf<T> {
// only executed in tests: ensure the member account is correct.
debug_assert!(
PoolMembers::<T>::get(member_account).expect("member must exist") == pool_member
);

let pool_account = Pallet::<T>::generate_bonded_account(pool_member.pool_id);
// if the pool doesn't have any pending slash, it implies the member also does not have any
// pending slash.
if T::StakeAdapter::pending_slash(&pool_account).is_zero() {
return Zero::zero()
}

// this is their actual held balance that may or may not have been slashed.
let actual_balance = T::StakeAdapter::member_delegation_balance(member_account);
// this is their balance in the pool
let expected_balance = pool_member.total_balance();
defensive_assert!(
actual_balance >= expected_balance,
"actual balance should always be greater or equal to the expected"
);

// return the amount to be slashed.
actual_balance.defensive_saturating_sub(expected_balance)
}

/// 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(
Expand Down Expand Up @@ -3805,6 +3827,62 @@ impl<T: Config> Pallet<T> {
Zero::zero()
}
}

/// Returns the unapplied slash of the pool.
///
/// Pending slash is only applicable with [`adapter::DelegateStake`] strategy.
pub fn api_pool_pending_slash(pool_id: PoolId) -> BalanceOf<T> {
T::StakeAdapter::pending_slash(&Self::generate_bonded_account(pool_id))
}

pub fn api_member_pending_slash(who: T::AccountId) -> BalanceOf<T> {
PoolMembers::<T>::get(who.clone())
.map(|pool_member| Self::member_pending_slash(&who, pool_member))
.unwrap_or_default()
}

/// Checks whether pool needs to be migrated to [`adapter::StakeStrategyType::Delegate`]. Only
/// applicable when the [`Config::StakeAdapter`] is [`adapter::DelegateStake`].
///
/// Useful to check this before calling [`Call::migrate_pool_to_delegate_stake`].
pub fn api_pool_needs_delegate_migration(pool_id: PoolId) -> bool {
// if the `Delegate` strategy is not used in the pallet, then no migration required.
if T::StakeAdapter::strategy_type() != adapter::StakeStrategyType::Delegate {
return false
}

let pool_account = Self::generate_bonded_account(pool_id);
// true if pool is still not migrated to `DelegateStake`.
T::StakeAdapter::pool_strategy(&pool_account) != adapter::StakeStrategyType::Delegate
}

/// Checks whether member delegation needs to be migrated to
/// [`adapter::StakeStrategyType::Delegate`]. Only applicable when the [`Config::StakeAdapter`]
/// is [`adapter::DelegateStake`].
///
/// Useful to check this before calling [`Call::migrate_delegation`].
pub fn api_member_needs_delegate_migration(who: T::AccountId) -> bool {
// if the `Delegate` strategy is not used in the pallet, then no migration required.
if T::StakeAdapter::strategy_type() != adapter::StakeStrategyType::Delegate {
return false
}

PoolMembers::<T>::get(&who)
.map(|pool_member| {
if Self::api_pool_needs_delegate_migration(pool_member.pool_id) {
// the pool needs to be migrated before members can be migrated.
return false
}

let member_balance = pool_member.total_balance();
let delegated_balance = T::StakeAdapter::member_delegation_balance(&who);

// if the member has no delegation but has some balance in the pool, then it needs
// to be migrated.
delegated_balance.is_zero() && !member_balance.is_zero()
})
.unwrap_or_default()
}
}

impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pallet<T> {
Expand Down
6 changes: 6 additions & 0 deletions substrate/frame/nomination-pools/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5021,6 +5021,10 @@ mod set_state {
Error::<Runtime>::NotSupported
);

// pending slash api should return zero as well.
assert_eq!(Pools::api_pool_pending_slash(1), 0);
assert_eq!(Pools::api_member_pending_slash(10), 0);

// When
assert_ok!(Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying));
// Then
Expand Down Expand Up @@ -7518,12 +7522,14 @@ mod delegate_stake {
);

// ensure pool 1 cannot be migrated.
assert!(!Pools::api_pool_needs_delegate_migration(1));
assert_noop!(
Pools::migrate_pool_to_delegate_stake(RuntimeOrigin::signed(10), 1),
Error::<Runtime>::NotSupported
);

// members cannot be migrated either.
assert!(!Pools::api_member_needs_delegate_migration(10));
assert_noop!(
Pools::migrate_delegation(RuntimeOrigin::signed(10), 11),
Error::<Runtime>::NotSupported
Expand Down
Loading