Skip to content

Commit

Permalink
Introduces: Delegated Staking Pallet (#3904)
Browse files Browse the repository at this point in the history
This is the second PR in preparation for
#454.

## Also see
- **Precursor** #3889.
- **Follow up** #3905.

Overall changes are documented here (lot more visual 😍):
https://hackmd.io/@ak0n/454-np-governance

## Changes
### Delegation Interface
Provides delegation primitives for staking. 

Introduces two new roles:
- Agent: These are accounts who receive delegation from other accounts
(delegators) and stakes on behalf of them. The funds are held in
delegator accounts.
- Delegator: Accounts who delegate their funds to an agent authorising
them to use it for staking.

Supports
- A way for delegators to add or withdraw delegation to an agent.
- A way for an agent to slash a delegator during a slashing event.

### Pallet Delegated Staking
- Implements `DelegationInterface`.
- Lazy slashing: Any slashes to an Agent is posted in a ledger but not
immediately slashed. The agent can call
`DelegationInterface::delegator_slash` to slash the member and clear the
corresponding slash from its ledger.
- Consumes `StakingInterface` to provide `CoreStaking` features. In
reality, this will be `pallet-staking`.
- Ensures bookkeeping for agent and delegator are correct but leaves the
management of reward and slash logic upto the consumer of this pallet.
- While it does not expose any calls yet, it is written with the intent
of exposing these primitives via extrinsics.

## TODO
- [x] Improve unit tests in the pallet.
- [x] Separate slash reward perbill for rewarding the slash reporters?
- [x] Review if we should add more events.

---------

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Gonçalo Pestana <g6pestana@gmail.com>
Co-authored-by: georgepisaltu <52418509+georgepisaltu@users.noreply.github.com>
  • Loading branch information
4 people authored May 15, 2024
1 parent e6d934c commit 4d47b44
Show file tree
Hide file tree
Showing 11 changed files with 2,480 additions and 1 deletion.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ members = [
"substrate/frame/contracts/uapi",
"substrate/frame/conviction-voting",
"substrate/frame/core-fellowship",
"substrate/frame/delegated-staking",
"substrate/frame/democracy",
"substrate/frame/election-provider-multi-phase",
"substrate/frame/election-provider-multi-phase/test-staking-e2e",
Expand Down
19 changes: 19 additions & 0 deletions prdoc/pr_3904.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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: Introduce pallet-delegated-staking

doc:
- audience: Runtime Dev
description: |
Adds a new pallet `delegated-staking` that allows delegators to delegate their funds to agents who can stake
these funds on behalf of them. This would be used by Nomination Pools to migrate into a delegation staking based
pool.

crates:
- name: pallet-delegated-staking
bump: patch
- name: pallet-staking
bump: patch
- name: sp-staking
bump: minor
69 changes: 69 additions & 0 deletions substrate/frame/delegated-staking/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
[package]
name = "pallet-delegated-staking"
version = "1.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage = "https://substrate.io"
repository.workspace = true
description = "FRAME delegated staking pallet"

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] }
frame-support = { path = "../support", default-features = false }
frame-system = { path = "../system", default-features = false }
scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
sp-std = { path = "../../primitives/std", default-features = false }
sp-runtime = { path = "../../primitives/runtime", default-features = false }
sp-staking = { path = "../../primitives/staking", default-features = false }

[dev-dependencies]
sp-core = { path = "../../primitives/core" }
sp-io = { path = "../../primitives/io" }
substrate-test-utils = { path = "../../test-utils" }
sp-tracing = { path = "../../primitives/tracing" }
pallet-staking = { path = "../staking" }
pallet-balances = { path = "../balances" }
pallet-timestamp = { path = "../timestamp" }
pallet-staking-reward-curve = { path = "../staking/reward-curve" }
frame-election-provider-support = { path = "../election-provider-support", default-features = false }

[features]
default = ["std"]
std = [
"codec/std",
"frame-election-provider-support/std",
"frame-support/std",
"frame-system/std",
"pallet-balances/std",
"pallet-staking/std",
"pallet-timestamp/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-staking/std",
"sp-std/std",
]
runtime-benchmarks = [
"frame-election-provider-support/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-staking/runtime-benchmarks",
"pallet-timestamp/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]
try-runtime = [
"frame-election-provider-support/try-runtime",
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/try-runtime",
"pallet-staking/try-runtime",
"pallet-timestamp/try-runtime",
"sp-runtime/try-runtime",
]
149 changes: 149 additions & 0 deletions substrate/frame/delegated-staking/src/impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Implementations of public traits, namely [`DelegationInterface`] and [`OnStakingUpdate`].

use super::*;
use sp_staking::{DelegationInterface, DelegationMigrator, OnStakingUpdate};

impl<T: Config> DelegationInterface for Pallet<T> {
type Balance = BalanceOf<T>;
type AccountId = T::AccountId;

/// Effective balance of the `Agent` account.
fn agent_balance(who: &Self::AccountId) -> Self::Balance {
Agent::<T>::get(who)
.map(|agent| agent.ledger.effective_balance())
.unwrap_or_default()
}

fn delegator_balance(delegator: &Self::AccountId) -> Self::Balance {
Delegation::<T>::get(delegator).map(|d| d.amount).unwrap_or_default()
}

/// Delegate funds to an `Agent`.
fn delegate(
who: &Self::AccountId,
agent: &Self::AccountId,
reward_account: &Self::AccountId,
amount: Self::Balance,
) -> DispatchResult {
Pallet::<T>::register_agent(
RawOrigin::Signed(agent.clone()).into(),
reward_account.clone(),
)?;

// Delegate the funds from who to the `Agent` account.
Pallet::<T>::delegate_to_agent(RawOrigin::Signed(who.clone()).into(), agent.clone(), amount)
}

/// Add more delegation to the `Agent` account.
fn delegate_extra(
who: &Self::AccountId,
agent: &Self::AccountId,
amount: Self::Balance,
) -> DispatchResult {
Pallet::<T>::delegate_to_agent(RawOrigin::Signed(who.clone()).into(), agent.clone(), amount)
}

/// Withdraw delegation of `delegator` to `Agent`.
///
/// If there are funds in `Agent` account that can be withdrawn, then those funds would be
/// unlocked/released in the delegator's account.
fn withdraw_delegation(
delegator: &Self::AccountId,
agent: &Self::AccountId,
amount: Self::Balance,
num_slashing_spans: u32,
) -> DispatchResult {
Pallet::<T>::release_delegation(
RawOrigin::Signed(agent.clone()).into(),
delegator.clone(),
amount,
num_slashing_spans,
)
}

/// 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)
}

fn delegator_slash(
agent: &Self::AccountId,
delegator: &Self::AccountId,
value: Self::Balance,
maybe_reporter: Option<Self::AccountId>,
) -> sp_runtime::DispatchResult {
Pallet::<T>::do_slash(agent.clone(), delegator.clone(), value, maybe_reporter)
}
}

impl<T: Config> DelegationMigrator for Pallet<T> {
type Balance = BalanceOf<T>;
type AccountId = T::AccountId;

fn migrate_nominator_to_agent(
agent: &Self::AccountId,
reward_account: &Self::AccountId,
) -> DispatchResult {
Pallet::<T>::migrate_to_agent(
RawOrigin::Signed(agent.clone()).into(),
reward_account.clone(),
)
}

fn migrate_delegation(
agent: &Self::AccountId,
delegator: &Self::AccountId,
value: Self::Balance,
) -> DispatchResult {
Pallet::<T>::migrate_delegation(
RawOrigin::Signed(agent.clone()).into(),
delegator.clone(),
value,
)
}
}

impl<T: Config> OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pallet<T> {
fn on_slash(
who: &T::AccountId,
_slashed_active: BalanceOf<T>,
_slashed_unlocking: &sp_std::collections::btree_map::BTreeMap<EraIndex, BalanceOf<T>>,
slashed_total: BalanceOf<T>,
) {
<Agents<T>>::mutate(who, |maybe_register| match maybe_register {
// if existing agent, register the slashed amount as pending slash.
Some(register) => register.pending_slash.saturating_accrue(slashed_total),
None => {
// nothing to do
},
});
}

fn on_withdraw(stash: &T::AccountId, amount: BalanceOf<T>) {
// if there is a withdraw to the agent, then add it to the unclaimed withdrawals.
let _ = Agent::<T>::get(stash)
// can't do anything if there is an overflow error. Just raise a defensive error.
.and_then(|agent| agent.add_unclaimed_withdraw(amount).defensive())
.map(|agent| agent.save());
}
}
Loading

0 comments on commit 4d47b44

Please sign in to comment.