diff --git a/Cargo.lock b/Cargo.lock
index 7ff2da4f5472..c91025ea34c6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10123,6 +10123,28 @@ dependencies = [
"sp-std 14.0.0",
]
+[[package]]
+name = "pallet-delegated-staking"
+version = "1.0.0"
+dependencies = [
+ "frame-election-provider-support",
+ "frame-support",
+ "frame-system",
+ "pallet-balances",
+ "pallet-staking",
+ "pallet-staking-reward-curve",
+ "pallet-timestamp",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-staking",
+ "sp-std 14.0.0",
+ "sp-tracing 16.0.0",
+ "substrate-test-utils",
+]
+
[[package]]
name = "pallet-democracy"
version = "28.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index 1440c2d497d3..dcf410daa1f0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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",
diff --git a/prdoc/pr_3904.prdoc b/prdoc/pr_3904.prdoc
new file mode 100644
index 000000000000..694f9b443877
--- /dev/null
+++ b/prdoc/pr_3904.prdoc
@@ -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
diff --git a/substrate/frame/delegated-staking/Cargo.toml b/substrate/frame/delegated-staking/Cargo.toml
new file mode 100644
index 000000000000..a9cbd758ed09
--- /dev/null
+++ b/substrate/frame/delegated-staking/Cargo.toml
@@ -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",
+]
diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs
new file mode 100644
index 000000000000..b1945b0ce376
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/impls.rs
@@ -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 .
+
+//! Implementations of public traits, namely [`DelegationInterface`] and [`OnStakingUpdate`].
+
+use super::*;
+use sp_staking::{DelegationInterface, DelegationMigrator, OnStakingUpdate};
+
+impl DelegationInterface for Pallet {
+ type Balance = BalanceOf;
+ type AccountId = T::AccountId;
+
+ /// Effective balance of the `Agent` account.
+ fn agent_balance(who: &Self::AccountId) -> Self::Balance {
+ Agent::::get(who)
+ .map(|agent| agent.ledger.effective_balance())
+ .unwrap_or_default()
+ }
+
+ fn delegator_balance(delegator: &Self::AccountId) -> Self::Balance {
+ Delegation::::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::::register_agent(
+ RawOrigin::Signed(agent.clone()).into(),
+ reward_account.clone(),
+ )?;
+
+ // Delegate the funds from who to the `Agent` account.
+ Pallet::::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::::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::::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::::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,
+ ) -> sp_runtime::DispatchResult {
+ Pallet::::do_slash(agent.clone(), delegator.clone(), value, maybe_reporter)
+ }
+}
+
+impl DelegationMigrator for Pallet {
+ type Balance = BalanceOf;
+ type AccountId = T::AccountId;
+
+ fn migrate_nominator_to_agent(
+ agent: &Self::AccountId,
+ reward_account: &Self::AccountId,
+ ) -> DispatchResult {
+ Pallet::::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::::migrate_delegation(
+ RawOrigin::Signed(agent.clone()).into(),
+ delegator.clone(),
+ value,
+ )
+ }
+}
+
+impl OnStakingUpdate> for Pallet {
+ fn on_slash(
+ who: &T::AccountId,
+ _slashed_active: BalanceOf,
+ _slashed_unlocking: &sp_std::collections::btree_map::BTreeMap>,
+ slashed_total: BalanceOf,
+ ) {
+ >::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) {
+ // if there is a withdraw to the agent, then add it to the unclaimed withdrawals.
+ let _ = Agent::::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());
+ }
+}
diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs
new file mode 100644
index 000000000000..210f69d9c839
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/lib.rs
@@ -0,0 +1,815 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! # Delegated Staking Pallet
+//!
+//! This pallet implements [`sp_staking::DelegationInterface`] that provides delegation
+//! functionality to `delegators` and `agents`. It is designed to be used in conjunction with
+//! [`StakingInterface`] and relies on [`Config::CoreStaking`] to provide primitive staking
+//! functions.
+//!
+//! Currently, it does not expose any dispatchable calls but is written with a vision to expose them
+//! in the future such that it can be utilised by any external account, off-chain entity or xcm
+//! `MultiLocation` such as a parachain or a smart contract.
+//!
+//! ## Key Terminologies
+//! - **Agent**: An account who accepts delegations from other accounts and act as an agent on their
+//! behalf for staking these delegated funds. Also, sometimes referred as `Delegatee`.
+//! - **Delegator**: An account who delegates their funds to an `agent` and authorises them to use
+//! it for staking.
+//! - **AgentLedger**: A data structure that holds important information about the `agent` such as
+//! total delegations they have received, any slashes posted to them, etc.
+//! - **Delegation**: A data structure that stores the amount of funds delegated to an `agent` by a
+//! `delegator`.
+//!
+//! ## Goals
+//!
+//! Direct nomination on the Staking pallet does not scale well. Nominations pools were created to
+//! address this by pooling delegator funds into one account and then staking it. This though had
+//! a very critical limitation that the funds were moved from delegator account to pool account
+//! and hence the delegator lost control over their funds for using it for other purposes such as
+//! governance. This pallet aims to solve this by extending the staking pallet to support a new
+//! primitive function: delegation of funds to an `agent` with the intent of staking. The agent can
+//! then stake the delegated funds to [`Config::CoreStaking`] on behalf of the delegators.
+//!
+//! ### Withdrawal Management
+//! Agent unbonding does not regulate ordering of consequent withdrawal for delegators. This is upto
+//! the consumer of this pallet to implement in what order unbondable funds from
+//! [`Config::CoreStaking`] can be withdrawn by the delegators.
+//!
+//! ### Reward and Slashing
+//! This pallet does not enforce any specific strategy for how rewards or slashes are applied. It
+//! is upto the `agent` account to decide how to apply the rewards and slashes.
+//!
+//! This importantly allows clients of this pallet to build their own strategies for reward/slashes.
+//! For example, an `agent` account can choose to first slash the reward pot before slashing the
+//! delegators. Or part of the reward can go to an insurance fund that can be used to cover any
+//! potential future slashes. The goal is to eventually allow foreign MultiLocations
+//! (smart contracts or pallets on another chain) to build their own pooled staking solutions
+//! similar to `NominationPools`.
+
+//! ## Core functions
+//!
+//! - Allow an account to receive delegations. See [`Pallet::register_agent`].
+//! - Delegate funds to an `agent` account. See [`Pallet::delegate_to_agent`].
+//! - Release delegated funds from an `agent` account to the `delegator`. See
+//! [`Pallet::release_delegation`].
+//! - Migrate a `Nominator` account to an `agent` account. See [`Pallet::migrate_to_agent`].
+//! Explained in more detail in the `Migration` section.
+//! - Migrate unclaimed delegated funds from `agent` to delegator. When a nominator migrates to an
+//! agent, the funds are held in a proxy account. This function allows the delegator to claim their
+//! share of the funds from the proxy account. See [`Pallet::migrate_delegation`].
+//!
+//! ## Lazy Slashing
+//! One of the reasons why direct nominators on staking pallet cannot scale well is because all
+//! nominators are slashed at the same time. This is expensive and needs to be bounded operation.
+//!
+//! This pallet implements a lazy slashing mechanism. Any slashes to the `agent` are posted in its
+//! `AgentLedger` as a pending slash. Since the actual amount is held in the multiple
+//! `delegator` accounts, this pallet has no way to know how to apply slash. It is the `agent`'s
+//! responsibility to apply slashes for each delegator, one at a time. Staking pallet ensures the
+//! pending slash never exceeds staked amount and would freeze further withdraws until all pending
+//! slashes are cleared.
+//!
+//! The user of this pallet can apply slash using
+//! [DelegationInterface::delegator_slash](sp_staking::DelegationInterface::delegator_slash).
+//!
+//! ## Migration from Nominator to Agent
+//! More details [here](https://hackmd.io/@ak0n/454-np-governance).
+//!
+//! ## Nomination Pool vs Delegation Staking
+//! This pallet is not a replacement for Nomination Pool but adds a new primitive in addition to
+//! staking pallet that can be used by Nomination Pool to support delegation based staking. It can
+//! be thought of as an extension to the Staking Pallet in relation to Nomination Pools.
+//! Technically, these changes could be made in one of those pallets as well but that would have
+//! meant significant refactoring and high chances of introducing a regression. With this approach,
+//! we can keep the existing pallets with minimal changes and introduce a new pallet that can be
+//! optionally used by Nomination Pool. The vision is to build this in a configurable way such that
+//! runtime can choose whether to use this pallet or not.
+//!
+//! With that said, following is the main difference between
+//! #### Nomination Pool without delegation support
+//! 1) transfer fund from delegator to pool account, and
+//! 2) stake from pool account as a direct nominator.
+//!
+//! #### Nomination Pool with delegation support
+//! 1) delegate fund from delegator to pool account, and
+//! 2) stake from pool account as an `Agent` account on the staking pallet.
+//!
+//! The difference being, in the second approach, the delegated funds will be locked in-place in
+//! user's account enabling them to participate in use cases that allows use of `held` funds such
+//! as participation in governance voting.
+//!
+//! Nomination pool still does all the heavy lifting around pool administration, reward
+//! distribution, lazy slashing and as such, is not meant to be replaced with this pallet.
+//!
+//! ## Limitations
+//! - Rewards can not be auto-compounded.
+//! - Slashes are lazy and hence there could be a period of time when an account can use funds for
+//! operations such as voting in governance even though they should be slashed.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![deny(rustdoc::broken_intra_doc_links)]
+
+mod impls;
+#[cfg(test)]
+mod mock;
+#[cfg(test)]
+mod tests;
+mod types;
+
+pub use pallet::*;
+
+use types::*;
+
+use frame_support::{
+ pallet_prelude::*,
+ traits::{
+ fungible::{
+ hold::{
+ Balanced as FunHoldBalanced, Inspect as FunHoldInspect, Mutate as FunHoldMutate,
+ },
+ Balanced, Inspect as FunInspect, Mutate as FunMutate,
+ },
+ tokens::{fungible::Credit, Fortitude, Precision, Preservation},
+ Defensive, DefensiveOption, Imbalance, OnUnbalanced,
+ },
+};
+use sp_runtime::{
+ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero},
+ ArithmeticError, DispatchResult, Perbill, RuntimeDebug, Saturating,
+};
+use sp_staking::{EraIndex, StakingInterface, StakingUnchecked};
+use sp_std::{convert::TryInto, prelude::*};
+
+pub type BalanceOf =
+ <::Currency as FunInspect<::AccountId>>::Balance;
+
+use frame_system::{ensure_signed, pallet_prelude::*, RawOrigin};
+
+#[frame_support::pallet]
+pub mod pallet {
+ use super::*;
+
+ #[pallet::pallet]
+ pub struct Pallet(PhantomData);
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// The overarching event type.
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+
+ /// Injected identifier for the pallet.
+ #[pallet::constant]
+ type PalletId: Get;
+
+ /// Currency type.
+ type Currency: FunHoldMutate
+ + FunMutate
+ + FunHoldBalanced;
+
+ /// Handler for the unbalanced reduction when slashing a delegator.
+ type OnSlash: OnUnbalanced>;
+
+ /// Fraction of the slash that is rewarded to the caller of pending slash to the agent.
+ #[pallet::constant]
+ type SlashRewardFraction: Get;
+
+ /// Overarching hold reason.
+ type RuntimeHoldReason: From;
+
+ /// Core staking implementation.
+ type CoreStaking: StakingUnchecked, AccountId = Self::AccountId>;
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// The account cannot perform this operation.
+ NotAllowed,
+ /// An existing staker cannot perform this action.
+ AlreadyStaking,
+ /// Reward Destination cannot be same as `Agent` account.
+ InvalidRewardDestination,
+ /// Delegation conditions are not met.
+ ///
+ /// Possible issues are
+ /// 1) Cannot delegate to self,
+ /// 2) Cannot delegate to multiple delegates.
+ InvalidDelegation,
+ /// The account does not have enough funds to perform the operation.
+ NotEnoughFunds,
+ /// Not an existing `Agent` account.
+ NotAgent,
+ /// Not a Delegator account.
+ NotDelegator,
+ /// Some corruption in internal state.
+ BadState,
+ /// Unapplied pending slash restricts operation on `Agent`.
+ UnappliedSlash,
+ /// `Agent` has no pending slash to be applied.
+ NothingToSlash,
+ /// Failed to withdraw amount from Core Staking.
+ WithdrawFailed,
+ /// Operation not supported by this pallet.
+ NotSupported,
+ }
+
+ /// A reason for placing a hold on funds.
+ #[pallet::composite_enum]
+ pub enum HoldReason {
+ /// Funds held for stake delegation to another account.
+ #[codec(index = 0)]
+ StakingDelegation,
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub (super) fn deposit_event)]
+ pub enum Event {
+ /// Funds delegated by a delegator.
+ Delegated { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf },
+ /// Funds released to a delegator.
+ Released { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf },
+ /// Funds slashed from a delegator.
+ Slashed { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf },
+ }
+
+ /// Map of Delegators to their `Delegation`.
+ ///
+ /// Implementation note: We are not using a double map with `delegator` and `agent` account
+ /// as keys since we want to restrict delegators to delegate only to one account at a time.
+ #[pallet::storage]
+ pub(crate) type Delegators =
+ CountedStorageMap<_, Twox64Concat, T::AccountId, Delegation, OptionQuery>;
+
+ /// Map of `Agent` to their `Ledger`.
+ #[pallet::storage]
+ pub(crate) type Agents =
+ CountedStorageMap<_, Twox64Concat, T::AccountId, AgentLedger, OptionQuery>;
+
+ // This pallet is not currently written with the intention of exposing any calls. But the
+ // functions defined in the following impl block should act as a good reference for how the
+ // exposed calls would look like when exposed.
+ impl Pallet {
+ /// Register an account to become a stake `Agent`. Sometimes also called a `Delegatee`.
+ ///
+ /// Delegators can authorize `Agent`s to stake on their behalf by delegating their funds to
+ /// them. The `Agent` can then use the delegated funds to stake to [`Config::CoreStaking`].
+ ///
+ /// An account that is directly staked to [`Config::CoreStaking`] cannot become an `Agent`.
+ /// However, they can migrate to become an agent using [`Self::migrate_to_agent`].
+ ///
+ /// Implementation note: This function allows any account to become an agent. It is
+ /// important though that accounts that call [`StakingUnchecked::virtual_bond`] are keyless
+ /// accounts. This is not a problem for now since this is only used by other pallets in the
+ /// runtime which use keyless account as agents. If we later want to expose this as a
+ /// dispatchable call, we should derive a sub-account from the caller and use that as the
+ /// agent account.
+ pub fn register_agent(
+ origin: OriginFor,
+ reward_account: T::AccountId,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+
+ // Existing `agent` cannot register again and a delegator cannot become an `agent`.
+ ensure!(!Self::is_agent(&who) && !Self::is_delegator(&who), Error::::NotAllowed);
+
+ // They cannot be already a direct staker in the staking pallet.
+ ensure!(!Self::is_direct_staker(&who), Error::::AlreadyStaking);
+
+ // Reward account cannot be same as `agent` account.
+ ensure!(reward_account != who, Error::::InvalidRewardDestination);
+
+ Self::do_register_agent(&who, &reward_account);
+ Ok(())
+ }
+
+ /// Migrate from a `Nominator` account to `Agent` account.
+ ///
+ /// The origin needs to
+ /// - be a `Nominator` with [`Config::CoreStaking`],
+ /// - not already an `Agent`,
+ ///
+ /// This function will create a proxy account to the agent called `proxy_delegator` and
+ /// transfer the directly staked amount by the agent to it. The `proxy_delegator` delegates
+ /// the funds to the origin making origin an `Agent` account. The real `delegator`
+ /// accounts of the origin can later migrate their funds using [Self::migrate_delegation] to
+ /// claim back their share of delegated funds from `proxy_delegator` to self.
+ ///
+ /// Any free fund in the agent's account will be marked as unclaimed withdrawal.
+ pub fn migrate_to_agent(
+ origin: OriginFor,
+ reward_account: T::AccountId,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+ // ensure who is a staker in `CoreStaking` but not already an agent or a delegator.
+ ensure!(
+ Self::is_direct_staker(&who) && !Self::is_agent(&who) && !Self::is_delegator(&who),
+ Error::::NotAllowed
+ );
+
+ // Reward account cannot be same as `agent` account.
+ ensure!(reward_account != who, Error::::InvalidRewardDestination);
+
+ Self::do_migrate_to_agent(&who, &reward_account)
+ }
+
+ /// Release previously delegated funds by delegator to origin.
+ ///
+ /// Only agents can call this.
+ ///
+ /// Tries to withdraw unbonded funds from `CoreStaking` if needed and release amount to
+ /// `delegator`.
+ pub fn release_delegation(
+ origin: OriginFor,
+ delegator: T::AccountId,
+ amount: BalanceOf,
+ num_slashing_spans: u32,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+ Self::do_release(&who, &delegator, amount, num_slashing_spans)
+ }
+
+ /// Migrate delegated funds that are held in `proxy_delegator` to the claiming `delegator`'s
+ /// account. If successful, the specified funds will be moved and delegated from `delegator`
+ /// account to the agent.
+ ///
+ /// This can be called by `agent` accounts that were previously a direct `Nominator` with
+ /// [`Config::CoreStaking`] and has some remaining unclaimed delegations.
+ ///
+ /// Internally, it moves some delegations from `proxy_delegator` account to `delegator`
+ /// account and reapplying the holds.
+ pub fn migrate_delegation(
+ origin: OriginFor,
+ delegator: T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let agent = ensure_signed(origin)?;
+
+ // Ensure they have minimum delegation.
+ ensure!(amount >= T::Currency::minimum_balance(), Error::::NotEnoughFunds);
+
+ // Ensure delegator is sane.
+ ensure!(!Self::is_agent(&delegator), Error::::NotAllowed);
+ ensure!(!Self::is_delegator(&delegator), Error::::NotAllowed);
+ ensure!(!Self::is_direct_staker(&delegator), Error::::AlreadyStaking);
+
+ // ensure agent is sane.
+ ensure!(Self::is_agent(&agent), Error::::NotAgent);
+
+ // and has enough delegated balance to migrate.
+ let proxy_delegator = Self::sub_account(AccountType::ProxyDelegator, agent);
+ let balance_remaining = Self::held_balance_of(&proxy_delegator);
+ ensure!(balance_remaining >= amount, Error::::NotEnoughFunds);
+
+ Self::do_migrate_delegation(&proxy_delegator, &delegator, amount)
+ }
+
+ /// Delegate given `amount` of tokens to an `Agent` account.
+ ///
+ /// If `origin` is the first time delegator, we add them to state. If they are already
+ /// delegating, we increase the delegation.
+ ///
+ /// Conditions:
+ /// - Delegators cannot delegate to more than one agent.
+ /// - The `agent` account should already be registered as such. See
+ /// [`Self::register_agent`].
+ pub fn delegate_to_agent(
+ origin: OriginFor,
+ agent: T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let delegator = ensure_signed(origin)?;
+
+ // ensure delegator is sane.
+ ensure!(
+ Delegation::::can_delegate(&delegator, &agent),
+ Error::::InvalidDelegation
+ );
+ ensure!(!Self::is_direct_staker(&delegator), Error::::AlreadyStaking);
+
+ // ensure agent is sane.
+ ensure!(Self::is_agent(&agent), Error::::NotAgent);
+
+ // add to delegation.
+ Self::do_delegate(&delegator, &agent, amount)?;
+
+ // bond the newly delegated amount to `CoreStaking`.
+ Self::do_bond(&agent, amount)
+ }
+ }
+
+ #[pallet::hooks]
+ impl Hooks> for Pallet {
+ #[cfg(feature = "try-runtime")]
+ fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> {
+ Self::do_try_state()
+ }
+ }
+}
+
+impl Pallet {
+ /// Derive a (keyless) pot account from the given agent account and account type.
+ pub(crate) fn sub_account(account_type: AccountType, agent: T::AccountId) -> T::AccountId {
+ T::PalletId::get().into_sub_account_truncating((account_type, agent.clone()))
+ }
+
+ /// Held balance of a delegator.
+ pub(crate) fn held_balance_of(who: &T::AccountId) -> BalanceOf {
+ T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), who)
+ }
+
+ /// Returns true if who is registered as an `Agent`.
+ fn is_agent(who: &T::AccountId) -> bool {
+ >::contains_key(who)
+ }
+
+ /// Returns true if who is delegating to an `Agent` account.
+ fn is_delegator(who: &T::AccountId) -> bool {
+ >::contains_key(who)
+ }
+
+ /// Returns true if who is already staking on [`Config::CoreStaking`].
+ fn is_direct_staker(who: &T::AccountId) -> bool {
+ T::CoreStaking::status(who).is_ok()
+ }
+
+ /// Registers a new agent in the system.
+ fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) {
+ AgentLedger::::new(reward_account).update(who);
+
+ // Agent does not hold balance of its own but this pallet will provide for this to exist.
+ // This is expected to be a keyless account and not created by any user directly so safe.
+ // TODO: Someday if we allow anyone to be an agent, we should take a deposit for
+ // being a delegator.
+ frame_system::Pallet::::inc_providers(who);
+ }
+
+ /// Migrate existing staker account `who` to an `Agent` account.
+ fn do_migrate_to_agent(who: &T::AccountId, reward_account: &T::AccountId) -> DispatchResult {
+ Self::do_register_agent(who, reward_account);
+
+ // We create a proxy delegator that will keep all the delegation funds until funds are
+ // transferred to actual delegator.
+ let proxy_delegator = Self::sub_account(AccountType::ProxyDelegator, who.clone());
+
+ // Keep proxy delegator alive until all funds are migrated.
+ frame_system::Pallet::::inc_providers(&proxy_delegator);
+
+ // Get current stake
+ let stake = T::CoreStaking::stake(who)?;
+
+ // release funds from core staking.
+ T::CoreStaking::migrate_to_virtual_staker(who);
+
+ // transfer just released staked amount plus any free amount.
+ let amount_to_transfer =
+ T::Currency::reducible_balance(who, Preservation::Expendable, Fortitude::Polite);
+
+ // This should never fail but if it does, it indicates bad state and we abort.
+ T::Currency::transfer(who, &proxy_delegator, amount_to_transfer, Preservation::Expendable)?;
+
+ T::CoreStaking::update_payee(who, reward_account)?;
+ // delegate all transferred funds back to agent.
+ Self::do_delegate(&proxy_delegator, who, amount_to_transfer)?;
+
+ // if the transferred/delegated amount was greater than the stake, mark the extra as
+ // unclaimed withdrawal.
+ let unclaimed_withdraws = amount_to_transfer
+ .checked_sub(&stake.total)
+ .defensive_ok_or(ArithmeticError::Underflow)?;
+
+ if !unclaimed_withdraws.is_zero() {
+ let mut ledger = AgentLedger::::get(who).ok_or(Error::::NotAgent)?;
+ ledger.unclaimed_withdrawals = ledger
+ .unclaimed_withdrawals
+ .checked_add(&unclaimed_withdraws)
+ .defensive_ok_or(ArithmeticError::Overflow)?;
+ ledger.update(who);
+ }
+
+ Ok(())
+ }
+
+ /// Bond `amount` to `agent_acc` in [`Config::CoreStaking`].
+ fn do_bond(agent_acc: &T::AccountId, amount: BalanceOf) -> DispatchResult {
+ let agent = Agent::::get(agent_acc)?;
+
+ let available_to_bond = agent.available_to_bond();
+ defensive_assert!(amount == available_to_bond, "not expected value to bond");
+
+ if agent.is_bonded() {
+ T::CoreStaking::bond_extra(&agent.key, amount)
+ } else {
+ T::CoreStaking::virtual_bond(&agent.key, amount, agent.reward_account())
+ }
+ }
+
+ /// Delegate `amount` from `delegator` to `agent`.
+ fn do_delegate(
+ delegator: &T::AccountId,
+ agent: &T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let mut ledger = AgentLedger::::get(agent).ok_or(Error::::NotAgent)?;
+ // try to hold the funds.
+ T::Currency::hold(&HoldReason::StakingDelegation.into(), delegator, amount)?;
+
+ let new_delegation_amount =
+ if let Some(existing_delegation) = Delegation::::get(delegator) {
+ ensure!(&existing_delegation.agent == agent, Error::::InvalidDelegation);
+ existing_delegation
+ .amount
+ .checked_add(&amount)
+ .ok_or(ArithmeticError::Overflow)?
+ } else {
+ amount
+ };
+
+ Delegation::::new(agent, new_delegation_amount).update_or_kill(delegator);
+ ledger.total_delegated =
+ ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
+ ledger.update(agent);
+
+ Self::deposit_event(Event::::Delegated {
+ agent: agent.clone(),
+ delegator: delegator.clone(),
+ amount,
+ });
+
+ Ok(())
+ }
+
+ /// Release `amount` of delegated funds from `agent` to `delegator`.
+ fn do_release(
+ who: &T::AccountId,
+ delegator: &T::AccountId,
+ amount: BalanceOf,
+ num_slashing_spans: u32,
+ ) -> DispatchResult {
+ let mut agent = Agent::::get(who)?;
+ let mut delegation = Delegation::::get(delegator).ok_or(Error::::NotDelegator)?;
+
+ // make sure delegation to be released is sound.
+ ensure!(&delegation.agent == who, Error::::NotAgent);
+ ensure!(delegation.amount >= amount, Error::::NotEnoughFunds);
+
+ // if we do not already have enough funds to be claimed, try withdraw some more.
+ // keep track if we killed the staker in the process.
+ let stash_killed = if agent.ledger.unclaimed_withdrawals < amount {
+ // withdraw account.
+ let killed = T::CoreStaking::withdraw_unbonded(who.clone(), num_slashing_spans)
+ .map_err(|_| Error::::WithdrawFailed)?;
+ // reload agent from storage since withdrawal might have changed the state.
+ agent = agent.refresh()?;
+ Some(killed)
+ } else {
+ None
+ };
+
+ // if we still do not have enough funds to release, abort.
+ ensure!(agent.ledger.unclaimed_withdrawals >= amount, Error::::NotEnoughFunds);
+
+ // Claim withdraw from agent. Kill agent if no delegation left.
+ // TODO: Ideally if there is a register, there should be an unregister that should
+ // clean up the agent. Can be improved in future.
+ if agent.remove_unclaimed_withdraw(amount)?.update_or_kill()? {
+ match stash_killed {
+ Some(killed) => {
+ // this implies we did a `CoreStaking::withdraw` before release. Ensure
+ // we killed the staker as well.
+ ensure!(killed, Error::::BadState);
+ },
+ None => {
+ // We did not do a `CoreStaking::withdraw` before release. Ensure staker is
+ // already killed in `CoreStaking`.
+ ensure!(T::CoreStaking::status(who).is_err(), Error::::BadState);
+ },
+ }
+
+ // Remove provider reference for `who`.
+ let _ = frame_system::Pallet::::dec_providers(who).defensive();
+ }
+
+ // book keep delegation
+ delegation.amount = delegation
+ .amount
+ .checked_sub(&amount)
+ .defensive_ok_or(ArithmeticError::Overflow)?;
+
+ // remove delegator if nothing delegated anymore
+ delegation.update_or_kill(delegator);
+
+ let released = T::Currency::release(
+ &HoldReason::StakingDelegation.into(),
+ delegator,
+ amount,
+ Precision::BestEffort,
+ )?;
+
+ defensive_assert!(released == amount, "hold should have been released fully");
+
+ Self::deposit_event(Event::::Released {
+ agent: who.clone(),
+ delegator: delegator.clone(),
+ amount,
+ });
+
+ Ok(())
+ }
+
+ /// Migrates delegation of `amount` from `source` account to `destination` account.
+ fn do_migrate_delegation(
+ source_delegator: &T::AccountId,
+ destination_delegator: &T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let mut source_delegation =
+ Delegators::::get(source_delegator).defensive_ok_or(Error::::BadState)?;
+
+ // some checks that must have already been checked before.
+ ensure!(source_delegation.amount >= amount, Error::::NotEnoughFunds);
+ debug_assert!(
+ !Self::is_delegator(destination_delegator) && !Self::is_agent(destination_delegator)
+ );
+
+ // update delegations
+ Delegation::::new(&source_delegation.agent, amount)
+ .update_or_kill(destination_delegator);
+
+ source_delegation.amount = source_delegation
+ .amount
+ .checked_sub(&amount)
+ .defensive_ok_or(Error::::BadState)?;
+
+ source_delegation.update_or_kill(source_delegator);
+
+ // release funds from source
+ let released = T::Currency::release(
+ &HoldReason::StakingDelegation.into(),
+ source_delegator,
+ amount,
+ Precision::BestEffort,
+ )?;
+
+ defensive_assert!(released == amount, "hold should have been released fully");
+
+ // transfer the released amount to `destination_delegator`.
+ let post_balance = T::Currency::transfer(
+ source_delegator,
+ destination_delegator,
+ amount,
+ Preservation::Expendable,
+ )
+ .map_err(|_| Error::::BadState)?;
+
+ // if balance is zero, clear provider for source (proxy) delegator.
+ if post_balance == Zero::zero() {
+ let _ = frame_system::Pallet::::dec_providers(source_delegator).defensive();
+ }
+
+ // hold the funds again in the new delegator account.
+ T::Currency::hold(&HoldReason::StakingDelegation.into(), destination_delegator, amount)?;
+
+ Ok(())
+ }
+
+ /// Take slash `amount` from agent's `pending_slash`counter and apply it to `delegator` account.
+ pub fn do_slash(
+ agent_acc: T::AccountId,
+ delegator: T::AccountId,
+ amount: BalanceOf,
+ maybe_reporter: Option,
+ ) -> DispatchResult {
+ let agent = Agent::::get(&agent_acc)?;
+ // ensure there is something to slash
+ ensure!(agent.ledger.pending_slash > Zero::zero(), Error::::NothingToSlash);
+
+ let mut delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?;
+ ensure!(delegation.agent == agent_acc, Error::::NotAgent);
+ ensure!(delegation.amount >= amount, Error::::NotEnoughFunds);
+
+ // slash delegator
+ let (mut credit, missing) =
+ T::Currency::slash(&HoldReason::StakingDelegation.into(), &delegator, amount);
+
+ defensive_assert!(missing.is_zero(), "slash should have been fully applied");
+
+ let actual_slash = credit.peek();
+
+ // remove the applied slashed amount from agent.
+ agent.remove_slash(actual_slash).save();
+ delegation.amount =
+ delegation.amount.checked_sub(&actual_slash).ok_or(ArithmeticError::Overflow)?;
+ delegation.update_or_kill(&delegator);
+
+ if let Some(reporter) = maybe_reporter {
+ let reward_payout: BalanceOf = T::SlashRewardFraction::get() * actual_slash;
+ let (reporter_reward, rest) = credit.split(reward_payout);
+
+ // credit is the amount that we provide to `T::OnSlash`.
+ credit = rest;
+
+ // reward reporter or drop it.
+ let _ = T::Currency::resolve(&reporter, reporter_reward);
+ }
+
+ T::OnSlash::on_unbalanced(credit);
+
+ Self::deposit_event(Event::::Slashed { agent: agent_acc, delegator, amount });
+
+ Ok(())
+ }
+
+ /// Total balance that is available for stake. Includes already staked amount.
+ #[cfg(test)]
+ pub(crate) fn stakeable_balance(who: &T::AccountId) -> BalanceOf {
+ Agent::::get(who)
+ .map(|agent| agent.ledger.stakeable_balance())
+ .unwrap_or_default()
+ }
+}
+
+#[cfg(any(test, feature = "try-runtime"))]
+use sp_std::collections::btree_map::BTreeMap;
+
+#[cfg(any(test, feature = "try-runtime"))]
+impl Pallet {
+ pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
+ // build map to avoid reading storage multiple times.
+ let delegation_map = Delegators::::iter().collect::>();
+ let ledger_map = Agents::::iter().collect::>();
+
+ Self::check_delegates(ledger_map.clone())?;
+ Self::check_delegators(delegation_map, ledger_map)?;
+
+ Ok(())
+ }
+
+ fn check_delegates(
+ ledgers: BTreeMap>,
+ ) -> Result<(), sp_runtime::TryRuntimeError> {
+ for (agent, ledger) in ledgers {
+ ensure!(
+ matches!(
+ T::CoreStaking::status(&agent).expect("agent should be bonded"),
+ sp_staking::StakerStatus::Nominator(_) | sp_staking::StakerStatus::Idle
+ ),
+ "agent should be bonded and not validator"
+ );
+
+ ensure!(
+ ledger.stakeable_balance() >=
+ T::CoreStaking::total_stake(&agent)
+ .expect("agent should exist as a nominator"),
+ "Cannot stake more than balance"
+ );
+ }
+
+ Ok(())
+ }
+
+ fn check_delegators(
+ delegations: BTreeMap>,
+ ledger: BTreeMap>,
+ ) -> Result<(), sp_runtime::TryRuntimeError> {
+ let mut delegation_aggregation = BTreeMap::>::new();
+ for (delegator, delegation) in delegations.iter() {
+ ensure!(
+ T::CoreStaking::status(delegator).is_err(),
+ "delegator should not be directly staked"
+ );
+ ensure!(!Self::is_agent(delegator), "delegator cannot be an agent");
+
+ delegation_aggregation
+ .entry(delegation.agent.clone())
+ .and_modify(|e| *e += delegation.amount)
+ .or_insert(delegation.amount);
+ }
+
+ for (agent, total_delegated) in delegation_aggregation {
+ ensure!(!Self::is_delegator(&agent), "agent cannot be delegator");
+
+ let ledger = ledger.get(&agent).expect("ledger should exist");
+ ensure!(
+ ledger.total_delegated == total_delegated,
+ "ledger total delegated should match delegations"
+ );
+ }
+
+ Ok(())
+ }
+}
diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs
new file mode 100644
index 000000000000..21a9fe6b2270
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/mock.rs
@@ -0,0 +1,308 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::{self as delegated_staking, types::Agent};
+use frame_support::{
+ assert_ok, derive_impl,
+ pallet_prelude::*,
+ parameter_types,
+ traits::{ConstU64, Currency},
+ PalletId,
+};
+
+use sp_runtime::{traits::IdentityLookup, BuildStorage, Perbill};
+
+use frame_election_provider_support::{
+ bounds::{ElectionBounds, ElectionBoundsBuilder},
+ onchain, SequentialPhragmen,
+};
+use frame_support::dispatch::RawOrigin;
+use pallet_staking::{ActiveEra, ActiveEraInfo, CurrentEra};
+use sp_staking::{Stake, StakingInterface};
+
+pub type T = Runtime;
+type Block = frame_system::mocking::MockBlock;
+pub type AccountId = u128;
+
+pub const GENESIS_VALIDATOR: AccountId = 1;
+pub const GENESIS_NOMINATOR_ONE: AccountId = 101;
+pub const GENESIS_NOMINATOR_TWO: AccountId = 102;
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
+impl frame_system::Config for Runtime {
+ type Block = Block;
+ type AccountData = pallet_balances::AccountData;
+ type AccountId = AccountId;
+ type Lookup = IdentityLookup;
+}
+
+impl pallet_timestamp::Config for Runtime {
+ type Moment = u64;
+ type OnTimestampSet = ();
+ type MinimumPeriod = ConstU64<5>;
+ type WeightInfo = ();
+}
+
+pub type Balance = u128;
+
+parameter_types! {
+ pub static ExistentialDeposit: Balance = 1;
+}
+impl pallet_balances::Config for Runtime {
+ type MaxLocks = ConstU32<128>;
+ type MaxReserves = ();
+ type ReserveIdentifier = [u8; 8];
+ type Balance = Balance;
+ type RuntimeEvent = RuntimeEvent;
+ type DustRemoval = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type AccountStore = System;
+ type WeightInfo = ();
+ type FreezeIdentifier = RuntimeFreezeReason;
+ type MaxFreezes = ConstU32<1>;
+ type RuntimeHoldReason = RuntimeHoldReason;
+ type RuntimeFreezeReason = RuntimeFreezeReason;
+}
+
+pallet_staking_reward_curve::build! {
+ const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!(
+ min_inflation: 0_025_000,
+ max_inflation: 0_100_000,
+ ideal_stake: 0_500_000,
+ falloff: 0_050_000,
+ max_piece_count: 40,
+ test_precision: 0_005_000,
+ );
+}
+
+parameter_types! {
+ pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
+ pub static BondingDuration: u32 = 3;
+ pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
+}
+pub struct OnChainSeqPhragmen;
+impl onchain::Config for OnChainSeqPhragmen {
+ type System = Runtime;
+ type Solver = SequentialPhragmen;
+ type DataProvider = Staking;
+ type WeightInfo = ();
+ type MaxWinners = ConstU32<100>;
+ type Bounds = ElectionsBoundsOnChain;
+}
+
+impl pallet_staking::Config for Runtime {
+ type Currency = Balances;
+ type CurrencyBalance = Balance;
+ type UnixTime = pallet_timestamp::Pallet;
+ type CurrencyToVote = ();
+ type RewardRemainder = ();
+ type RuntimeEvent = RuntimeEvent;
+ type Slash = ();
+ type Reward = ();
+ type SessionsPerEra = ConstU32<1>;
+ type SlashDeferDuration = ();
+ type AdminOrigin = frame_system::EnsureRoot;
+ type BondingDuration = BondingDuration;
+ type SessionInterface = ();
+ type EraPayout = pallet_staking::ConvertCurve;
+ type NextNewSession = ();
+ type HistoryDepth = ConstU32<84>;
+ type MaxExposurePageSize = ConstU32<64>;
+ type ElectionProvider = onchain::OnChainExecution;
+ type GenesisElectionProvider = Self::ElectionProvider;
+ type VoterList = pallet_staking::UseNominatorsAndValidatorsMap;
+ type TargetList = pallet_staking::UseValidatorsMap;
+ type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
+ type MaxUnlockingChunks = ConstU32<10>;
+ type MaxControllersInDeprecationBatch = ConstU32<100>;
+ type EventListeners = DelegatedStaking;
+ type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
+ type WeightInfo = ();
+ type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
+}
+
+parameter_types! {
+ pub const DelegatedStakingPalletId: PalletId = PalletId(*b"py/dlstk");
+ pub const SlashRewardFraction: Perbill = Perbill::from_percent(10);
+}
+impl delegated_staking::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type PalletId = DelegatedStakingPalletId;
+ type Currency = Balances;
+ type OnSlash = ();
+ type SlashRewardFraction = SlashRewardFraction;
+ type RuntimeHoldReason = RuntimeHoldReason;
+ type CoreStaking = Staking;
+}
+
+parameter_types! {
+ pub static MaxUnbonding: u32 = 8;
+}
+
+frame_support::construct_runtime!(
+ pub enum Runtime {
+ System: frame_system,
+ Timestamp: pallet_timestamp,
+ Balances: pallet_balances,
+ Staking: pallet_staking,
+ DelegatedStaking: delegated_staking,
+ }
+);
+
+#[derive(Default)]
+pub struct ExtBuilder {}
+
+impl ExtBuilder {
+ fn build(self) -> sp_io::TestExternalities {
+ sp_tracing::try_init_simple();
+ let mut storage =
+ frame_system::GenesisConfig::::default().build_storage().unwrap();
+
+ let _ = pallet_balances::GenesisConfig:: {
+ balances: vec![
+ (GENESIS_VALIDATOR, 10000),
+ (GENESIS_NOMINATOR_ONE, 1000),
+ (GENESIS_NOMINATOR_TWO, 2000),
+ ],
+ }
+ .assimilate_storage(&mut storage);
+
+ let stakers = vec![
+ (
+ GENESIS_VALIDATOR,
+ GENESIS_VALIDATOR,
+ 1000,
+ sp_staking::StakerStatus::::Validator,
+ ),
+ (
+ GENESIS_NOMINATOR_ONE,
+ GENESIS_NOMINATOR_ONE,
+ 100,
+ sp_staking::StakerStatus::::Nominator(vec![1]),
+ ),
+ (
+ GENESIS_NOMINATOR_TWO,
+ GENESIS_NOMINATOR_TWO,
+ 200,
+ sp_staking::StakerStatus::::Nominator(vec![1]),
+ ),
+ ];
+
+ let _ = pallet_staking::GenesisConfig:: {
+ stakers: stakers.clone(),
+ // ideal validator count
+ validator_count: 2,
+ minimum_validator_count: 1,
+ invulnerables: vec![],
+ slash_reward_fraction: Perbill::from_percent(10),
+ min_nominator_bond: ExistentialDeposit::get(),
+ min_validator_bond: ExistentialDeposit::get(),
+ ..Default::default()
+ }
+ .assimilate_storage(&mut storage);
+
+ let mut ext = sp_io::TestExternalities::from(storage);
+
+ ext.execute_with(|| {
+ // for events to be deposited.
+ frame_system::Pallet::::set_block_number(1);
+ // set era for staking.
+ start_era(0);
+ });
+
+ ext
+ }
+ pub fn build_and_execute(self, test: impl FnOnce()) {
+ sp_tracing::try_init_simple();
+ let mut ext = self.build();
+ ext.execute_with(test);
+ ext.execute_with(|| {
+ #[cfg(feature = "try-runtime")]
+ >::try_state(
+ frame_system::Pallet::::block_number(),
+ frame_support::traits::TryStateSelect::All,
+ )
+ .unwrap();
+ #[cfg(not(feature = "try-runtime"))]
+ DelegatedStaking::do_try_state().unwrap();
+ });
+ }
+}
+
+/// fund and return who.
+pub(crate) fn fund(who: &AccountId, amount: Balance) {
+ let _ = Balances::deposit_creating(who, amount);
+}
+
+/// Sets up delegation for passed delegators, returns total delegated amount.
+///
+/// `delegate_amount` is incremented by the amount `increment` starting with `base_delegate_amount`
+/// from lower index to higher index of delegators.
+pub(crate) fn setup_delegation_stake(
+ agent: AccountId,
+ reward_acc: AccountId,
+ delegators: Vec,
+ base_delegate_amount: Balance,
+ increment: Balance,
+) -> Balance {
+ fund(&agent, 100);
+ assert_ok!(DelegatedStaking::register_agent(RawOrigin::Signed(agent).into(), reward_acc));
+ let mut delegated_amount: Balance = 0;
+ for (index, delegator) in delegators.iter().enumerate() {
+ let amount_to_delegate = base_delegate_amount + increment * index as Balance;
+ delegated_amount += amount_to_delegate;
+
+ fund(delegator, amount_to_delegate + ExistentialDeposit::get());
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(*delegator).into(),
+ agent,
+ amount_to_delegate
+ ));
+ }
+
+ // sanity checks
+ assert_eq!(DelegatedStaking::stakeable_balance(&agent), delegated_amount);
+ assert_eq!(Agent::::get(&agent).unwrap().available_to_bond(), 0);
+
+ delegated_amount
+}
+
+pub(crate) fn start_era(era: sp_staking::EraIndex) {
+ CurrentEra::::set(Some(era));
+ ActiveEra::::set(Some(ActiveEraInfo { index: era, start: None }));
+}
+
+pub(crate) fn eq_stake(who: AccountId, total: Balance, active: Balance) -> bool {
+ Staking::stake(&who).unwrap() == Stake { total, active } &&
+ get_agent(&who).ledger.stakeable_balance() == total
+}
+
+pub(crate) fn get_agent(agent: &AccountId) -> Agent {
+ Agent::::get(agent).expect("delegate should exist")
+}
+
+parameter_types! {
+ static ObservedEventsDelegatedStaking: usize = 0;
+}
+
+#[allow(unused)]
+pub(crate) fn events_since_last_call() -> Vec> {
+ let events = System::read_events_for_pallet::>();
+ let already_seen = ObservedEventsDelegatedStaking::get();
+ ObservedEventsDelegatedStaking::set(events.len());
+ events.into_iter().skip(already_seen).collect()
+}
diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs
new file mode 100644
index 000000000000..1f36f655beb8
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/tests.rs
@@ -0,0 +1,685 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests for pallet-delegated-staking.
+
+use super::*;
+use crate::mock::*;
+use frame_support::{assert_noop, assert_ok, traits::fungible::InspectHold};
+use pallet_staking::Error as StakingError;
+use sp_staking::DelegationInterface;
+
+#[test]
+fn create_an_agent_with_first_delegator() {
+ ExtBuilder::default().build_and_execute(|| {
+ let agent: AccountId = 200;
+ let reward_account: AccountId = 201;
+ let delegator: AccountId = 202;
+
+ // set intention to accept delegation.
+ fund(&agent, 1000);
+ assert_ok!(DelegatedStaking::register_agent(
+ RawOrigin::Signed(agent).into(),
+ reward_account
+ ));
+
+ // delegate to this account
+ fund(&delegator, 1000);
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator).into(),
+ agent,
+ 100
+ ));
+
+ // verify
+ assert!(DelegatedStaking::is_agent(&agent));
+ assert_eq!(DelegatedStaking::stakeable_balance(&agent), 100);
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &delegator),
+ 100
+ );
+ assert_eq!(DelegatedStaking::held_balance_of(&delegator), 100);
+ });
+}
+
+#[test]
+fn cannot_become_agent() {
+ ExtBuilder::default().build_and_execute(|| {
+ // cannot set reward account same as agent account
+ assert_noop!(
+ DelegatedStaking::register_agent(RawOrigin::Signed(100).into(), 100),
+ Error::::InvalidRewardDestination
+ );
+
+ // an existing validator cannot become agent
+ assert_noop!(
+ DelegatedStaking::register_agent(
+ RawOrigin::Signed(mock::GENESIS_VALIDATOR).into(),
+ 100
+ ),
+ Error::::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::::AlreadyStaking
+ );
+ assert_noop!(
+ DelegatedStaking::register_agent(
+ RawOrigin::Signed(mock::GENESIS_NOMINATOR_TWO).into(),
+ 100
+ ),
+ Error::::AlreadyStaking
+ );
+ });
+}
+
+#[test]
+fn create_multiple_delegators() {
+ ExtBuilder::default().build_and_execute(|| {
+ let agent: AccountId = 200;
+ let reward_account: AccountId = 201;
+
+ // stakeable balance is 0 for non agent
+ fund(&agent, 1000);
+ assert!(!DelegatedStaking::is_agent(&agent));
+ assert_eq!(DelegatedStaking::stakeable_balance(&agent), 0);
+
+ // set intention to accept delegation.
+ assert_ok!(DelegatedStaking::register_agent(
+ RawOrigin::Signed(agent).into(),
+ reward_account
+ ));
+
+ // create 100 delegators
+ for i in 202..302 {
+ fund(&i, 100 + ExistentialDeposit::get());
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(i).into(),
+ agent,
+ 100
+ ));
+ // Balance of 100 held on delegator account for delegating to the agent.
+ assert_eq!(Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &i), 100);
+ }
+
+ // verify
+ assert!(DelegatedStaking::is_agent(&agent));
+ assert_eq!(DelegatedStaking::stakeable_balance(&agent), 100 * 100);
+ });
+}
+
+#[test]
+fn agent_restrictions() {
+ // Similar to creating a nomination pool
+ ExtBuilder::default().build_and_execute(|| {
+ let agent_one = 200;
+ let delegator_one = 210;
+ fund(&agent_one, 100);
+ assert_ok!(DelegatedStaking::register_agent(
+ RawOrigin::Signed(agent_one).into(),
+ agent_one + 1
+ ));
+ fund(&delegator_one, 200);
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator_one).into(),
+ agent_one,
+ 100
+ ));
+
+ let agent_two = 300;
+ let delegator_two = 310;
+ fund(&agent_two, 100);
+ assert_ok!(DelegatedStaking::register_agent(
+ RawOrigin::Signed(agent_two).into(),
+ agent_two + 1
+ ));
+ fund(&delegator_two, 200);
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator_two).into(),
+ agent_two,
+ 100
+ ));
+
+ // agent one tries to delegate to agent 2
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(RawOrigin::Signed(agent_one).into(), agent_two, 10),
+ Error::::InvalidDelegation
+ );
+
+ // agent one tries to delegate to a delegator
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(agent_one).into(),
+ delegator_one,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(agent_one).into(),
+ delegator_two,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // delegator one tries to delegate to agent 2 as well (it already delegates to agent
+ // 1)
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator_one).into(),
+ agent_two,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // cannot delegate to non agents.
+ let non_agent = 201;
+ // give it some funds
+ fund(&non_agent, 200);
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator_one).into(),
+ non_agent,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // cannot delegate to a delegator
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator_one).into(),
+ delegator_two,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // delegator cannot delegate to self
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator_one).into(),
+ delegator_one,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // agent cannot delegate to self
+ assert_noop!(
+ DelegatedStaking::delegate_to_agent(RawOrigin::Signed(agent_one).into(), agent_one, 10),
+ Error::::InvalidDelegation
+ );
+ });
+}
+
+#[test]
+fn apply_pending_slash() {
+ ExtBuilder::default().build_and_execute(|| {
+ start_era(1);
+ let agent: AccountId = 200;
+ let reward_acc: AccountId = 201;
+ let delegators: Vec = (301..=350).collect();
+ let reporter: AccountId = 400;
+
+ let total_staked = setup_delegation_stake(agent, reward_acc, delegators.clone(), 10, 10);
+
+ start_era(4);
+ // slash half of the stake
+ pallet_staking::slashing::do_slash::(
+ &agent,
+ total_staked / 2,
+ &mut Default::default(),
+ &mut Default::default(),
+ 3,
+ );
+
+ // agent cannot slash an account that is not its delegator.
+ setup_delegation_stake(210, 211, (351..=352).collect(), 100, 0);
+ assert_noop!(
+ ::delegator_slash(&agent, &351, 1, Some(400)),
+ Error::::NotAgent
+ );
+ // or a non delegator account
+ fund(&353, 100);
+ assert_noop!(
+ ::delegator_slash(&agent, &353, 1, Some(400)),
+ Error::::NotDelegator
+ );
+
+ // ensure bookkept pending slash is correct.
+ assert_eq!(get_agent(&agent).ledger.pending_slash, total_staked / 2);
+ let mut old_reporter_balance = Balances::free_balance(reporter);
+
+ // lets apply the pending slash on delegators.
+ for i in delegators {
+ // balance before slash
+ let initial_pending_slash = get_agent(&agent).ledger.pending_slash;
+ assert!(initial_pending_slash > 0);
+ let unslashed_balance = DelegatedStaking::held_balance_of(&i);
+ let slash = unslashed_balance / 2;
+ // slash half of delegator's delegation.
+ assert_ok!(::delegator_slash(
+ &agent,
+ &i,
+ slash,
+ Some(400)
+ ));
+
+ // balance after slash.
+ assert_eq!(DelegatedStaking::held_balance_of(&i), unslashed_balance - slash);
+ // pending slash is reduced by the amount slashed.
+ assert_eq!(get_agent(&agent).ledger.pending_slash, initial_pending_slash - slash);
+ // reporter get 10% of the slash amount.
+ assert_eq!(
+ Balances::free_balance(reporter) - old_reporter_balance,
+ ::slash_reward_fraction() * slash,
+ );
+ // update old balance
+ old_reporter_balance = Balances::free_balance(reporter);
+ }
+
+ // nothing to slash anymore
+ assert_eq!(get_agent(&agent).ledger.pending_slash, 0);
+
+ // cannot slash anymore
+ assert_noop!(
+ ::delegator_slash(&agent, &350, 1, None),
+ Error::::NothingToSlash
+ );
+ });
+}
+
+/// Integration tests with pallet-staking.
+mod staking_integration {
+ use super::*;
+ use pallet_staking::RewardDestination;
+ use sp_staking::Stake;
+
+ #[test]
+ fn bond() {
+ ExtBuilder::default().build_and_execute(|| {
+ let agent: AccountId = 99;
+ let reward_acc: AccountId = 100;
+ assert_eq!(Staking::status(&agent), Err(StakingError::::NotStash.into()));
+
+ // set intention to become an agent
+ fund(&agent, 100);
+ assert_ok!(DelegatedStaking::register_agent(
+ RawOrigin::Signed(agent).into(),
+ reward_acc
+ ));
+ assert_eq!(DelegatedStaking::stakeable_balance(&agent), 0);
+
+ let mut delegated_balance: Balance = 0;
+
+ // set some delegations
+ for delegator in 200..250 {
+ fund(&delegator, 200);
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(delegator).into(),
+ agent,
+ 100
+ ));
+ delegated_balance += 100;
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &delegator),
+ 100
+ );
+ assert_eq!(DelegatedStaking::delegator_balance(&delegator), 100);
+
+ let agent_obj = get_agent(&agent);
+ assert_eq!(agent_obj.ledger.stakeable_balance(), delegated_balance);
+ assert_eq!(agent_obj.available_to_bond(), 0);
+ assert_eq!(agent_obj.bonded_stake(), delegated_balance);
+ }
+
+ assert_eq!(Staking::stake(&agent).unwrap(), Stake { total: 50 * 100, active: 50 * 100 })
+ });
+ }
+
+ #[test]
+ fn withdraw_test() {
+ ExtBuilder::default().build_and_execute(|| {
+ // initial era
+ start_era(1);
+ let agent: AccountId = 200;
+ let reward_acc: AccountId = 201;
+ let delegators: Vec = (301..=350).collect();
+ let total_staked =
+ setup_delegation_stake(agent, reward_acc, delegators.clone(), 10, 10);
+
+ // lets go to a new era
+ start_era(2);
+
+ assert!(eq_stake(agent, total_staked, total_staked));
+ // Withdrawing without unbonding would fail.
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 301, 50, 0),
+ Error::::NotEnoughFunds
+ );
+
+ // 305 wants to unbond 50 in era 2, withdrawable in era 5.
+ assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 50));
+
+ // 310 wants to unbond 100 in era 3, withdrawable in era 6.
+ start_era(3);
+ assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 100));
+
+ // 320 wants to unbond 200 in era 4, withdrawable in era 7.
+ start_era(4);
+ assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 200));
+
+ // active stake is now reduced..
+ let expected_active = total_staked - (50 + 100 + 200);
+ assert!(eq_stake(agent, total_staked, expected_active));
+
+ // nothing to withdraw at era 4
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 305, 50, 0),
+ Error::::NotEnoughFunds
+ );
+
+ assert_eq!(get_agent(&agent).available_to_bond(), 0);
+ // full amount is still delegated
+ assert_eq!(get_agent(&agent).ledger.effective_balance(), total_staked);
+
+ start_era(5);
+ // at era 5, 50 tokens are withdrawable, cannot withdraw more.
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 305, 51, 0),
+ Error::::NotEnoughFunds
+ );
+ // less is possible
+ assert_ok!(DelegatedStaking::release_delegation(
+ RawOrigin::Signed(agent).into(),
+ 305,
+ 30,
+ 0
+ ));
+ assert_ok!(DelegatedStaking::release_delegation(
+ RawOrigin::Signed(agent).into(),
+ 305,
+ 20,
+ 0
+ ));
+
+ // Lets go to future era where everything is unbonded. Withdrawable amount: 100 + 200
+ start_era(7);
+ // 305 has no more amount delegated so it cannot withdraw.
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 305, 5, 0),
+ Error::::NotDelegator
+ );
+ // 309 is an active delegator but has total delegation of 90, so it cannot withdraw more
+ // than that.
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 309, 91, 0),
+ Error::::NotEnoughFunds
+ );
+ // 310 cannot withdraw more than delegated funds.
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 310, 101, 0),
+ Error::::NotEnoughFunds
+ );
+ // but can withdraw all its delegation amount.
+ assert_ok!(DelegatedStaking::release_delegation(
+ RawOrigin::Signed(agent).into(),
+ 310,
+ 100,
+ 0
+ ));
+ // 320 can withdraw all its delegation amount.
+ assert_ok!(DelegatedStaking::release_delegation(
+ RawOrigin::Signed(agent).into(),
+ 320,
+ 200,
+ 0
+ ));
+
+ // cannot withdraw anything more..
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 301, 1, 0),
+ Error::::NotEnoughFunds
+ );
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 350, 1, 0),
+ Error::::NotEnoughFunds
+ );
+ });
+ }
+
+ #[test]
+ fn withdraw_happens_with_unbonded_balance_first() {
+ ExtBuilder::default().build_and_execute(|| {
+ start_era(1);
+ let agent = 200;
+ setup_delegation_stake(agent, 201, (300..350).collect(), 100, 0);
+
+ // verify withdraw not possible yet
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 300, 100, 0),
+ Error::::NotEnoughFunds
+ );
+
+ // fill up unlocking chunks in core staking.
+ // 10 is the max chunks
+ for i in 2..=11 {
+ start_era(i);
+ assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 10));
+ // no withdrawals from core staking yet.
+ assert_eq!(get_agent(&agent).ledger.unclaimed_withdrawals, 0);
+ }
+
+ // another unbond would trigger withdrawal
+ start_era(12);
+ assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 10));
+
+ // 8 previous unbonds would be withdrawn as they were already unlocked. Unlocking period
+ // is 3 eras.
+ assert_eq!(get_agent(&agent).ledger.unclaimed_withdrawals, 8 * 10);
+
+ // release some delegation now.
+ assert_ok!(DelegatedStaking::release_delegation(
+ RawOrigin::Signed(agent).into(),
+ 300,
+ 40,
+ 0
+ ));
+ assert_eq!(get_agent(&agent).ledger.unclaimed_withdrawals, 80 - 40);
+
+ // cannot release more than available
+ assert_noop!(
+ DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 300, 50, 0),
+ Error::::NotEnoughFunds
+ );
+ assert_ok!(DelegatedStaking::release_delegation(
+ RawOrigin::Signed(agent).into(),
+ 300,
+ 40,
+ 0
+ ));
+
+ assert_eq!(DelegatedStaking::held_balance_of(&300), 100 - 80);
+ });
+ }
+
+ #[test]
+ fn reward_destination_restrictions() {
+ ExtBuilder::default().build_and_execute(|| {
+ // give some funds to 200
+ fund(&200, 1000);
+ let balance_200 = Balances::free_balance(200);
+
+ // `Agent` account cannot be reward destination
+ assert_noop!(
+ DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 200),
+ Error::::InvalidRewardDestination
+ );
+
+ // different reward account works
+ assert_ok!(DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 201));
+ // add some delegations to it
+ fund(&300, 1000);
+ assert_ok!(DelegatedStaking::delegate_to_agent(
+ RawOrigin::Signed(300).into(),
+ 200,
+ 100
+ ));
+
+ // update_payee to self fails.
+ assert_noop!(
+ ::update_payee(&200, &200),
+ StakingError::::RewardDestinationRestricted
+ );
+
+ // passing correct reward destination works
+ assert_ok!(::update_payee(&200, &201));
+
+ // amount is staked correctly
+ assert!(eq_stake(200, 100, 100));
+ assert_eq!(get_agent(&200).available_to_bond(), 0);
+ assert_eq!(get_agent(&200).ledger.effective_balance(), 100);
+
+ // free balance of delegate is untouched
+ assert_eq!(Balances::free_balance(200), balance_200);
+ });
+ }
+
+ #[test]
+ fn agent_restrictions() {
+ ExtBuilder::default().build_and_execute(|| {
+ setup_delegation_stake(200, 201, (202..203).collect(), 100, 0);
+
+ // Registering again is noop
+ assert_noop!(
+ DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 201),
+ Error::::NotAllowed
+ );
+ // a delegator cannot become delegate
+ assert_noop!(
+ DelegatedStaking::register_agent(RawOrigin::Signed(202).into(), 203),
+ Error::::NotAllowed
+ );
+ // existing staker cannot become a delegate
+ assert_noop!(
+ DelegatedStaking::register_agent(
+ RawOrigin::Signed(GENESIS_NOMINATOR_ONE).into(),
+ 201
+ ),
+ Error::::AlreadyStaking
+ );
+ assert_noop!(
+ DelegatedStaking::register_agent(RawOrigin::Signed(GENESIS_VALIDATOR).into(), 201),
+ Error::::AlreadyStaking
+ );
+ });
+ }
+
+ #[test]
+ fn migration_works() {
+ ExtBuilder::default().build_and_execute(|| {
+ // add a nominator
+ let staked_amount = 4000;
+ let agent_amount = 5000;
+ fund(&200, agent_amount);
+
+ assert_ok!(Staking::bond(
+ RuntimeOrigin::signed(200),
+ staked_amount,
+ RewardDestination::Account(201)
+ ));
+ assert_ok!(Staking::nominate(RuntimeOrigin::signed(200), vec![GENESIS_VALIDATOR],));
+ let init_stake = Staking::stake(&200).unwrap();
+
+ // scenario: 200 is a pool account, and the stake comes from its 4 delegators (300..304)
+ // in equal parts. lets try to migrate this nominator into delegate based stake.
+
+ // all balance currently is in 200
+ assert_eq!(Balances::free_balance(200), agent_amount);
+
+ // to migrate, nominator needs to set an account as a proxy delegator where staked funds
+ // will be moved and delegated back to this old nominator account. This should be funded
+ // with at least ED.
+ let proxy_delegator = DelegatedStaking::sub_account(AccountType::ProxyDelegator, 200);
+
+ assert_ok!(DelegatedStaking::migrate_to_agent(RawOrigin::Signed(200).into(), 201));
+
+ // verify all went well
+ let mut expected_proxy_delegated_amount = agent_amount;
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &proxy_delegator),
+ expected_proxy_delegated_amount
+ );
+ // stake amount is transferred from delegate to proxy delegator account.
+ assert_eq!(Balances::free_balance(200), 0);
+ assert_eq!(Staking::stake(&200).unwrap(), init_stake);
+ assert_eq!(get_agent(&200).ledger.effective_balance(), agent_amount);
+ assert_eq!(get_agent(&200).available_to_bond(), 0);
+ assert_eq!(get_agent(&200).ledger.unclaimed_withdrawals, agent_amount - staked_amount);
+
+ // now lets migrate the delegators
+ let delegator_share = agent_amount / 4;
+ for delegator in 300..304 {
+ assert_eq!(Balances::free_balance(delegator), 0);
+ // fund them with ED
+ fund(&delegator, ExistentialDeposit::get());
+ // migrate 1/4th amount into each delegator
+ assert_ok!(DelegatedStaking::migrate_delegation(
+ RawOrigin::Signed(200).into(),
+ delegator,
+ delegator_share
+ ));
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &delegator),
+ delegator_share
+ );
+ expected_proxy_delegated_amount -= delegator_share;
+ assert_eq!(
+ Balances::balance_on_hold(
+ &HoldReason::StakingDelegation.into(),
+ &proxy_delegator
+ ),
+ expected_proxy_delegated_amount
+ );
+
+ // delegate stake is unchanged.
+ assert_eq!(Staking::stake(&200).unwrap(), init_stake);
+ assert_eq!(get_agent(&200).ledger.effective_balance(), agent_amount);
+ assert_eq!(get_agent(&200).available_to_bond(), 0);
+ assert_eq!(
+ get_agent(&200).ledger.unclaimed_withdrawals,
+ agent_amount - staked_amount
+ );
+ }
+
+ // cannot use migrate delegator anymore
+ assert_noop!(
+ DelegatedStaking::migrate_delegation(RawOrigin::Signed(200).into(), 305, 1),
+ Error::::NotEnoughFunds
+ );
+ });
+ }
+}
diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs
new file mode 100644
index 000000000000..0bfc23281dfe
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/types.rs
@@ -0,0 +1,292 @@
+// 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 .
+
+//! Basic types used in delegated staking.
+
+use super::*;
+use frame_support::traits::DefensiveSaturating;
+
+/// The type of pot account being created.
+#[derive(Encode, Decode)]
+pub(crate) enum AccountType {
+ /// A proxy delegator account created for a nominator who migrated to an `Agent` account.
+ ///
+ /// Funds for unmigrated `delegator` accounts of the `Agent` are kept here.
+ ProxyDelegator,
+}
+
+/// Information about delegation of a `delegator`.
+#[derive(Default, Encode, Clone, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
+#[scale_info(skip_type_params(T))]
+pub struct Delegation {
+ /// The target of delegation.
+ pub agent: T::AccountId,
+ /// The amount delegated.
+ pub amount: BalanceOf,
+}
+
+impl Delegation {
+ /// Get delegation of a `delegator`.
+ pub(crate) fn get(delegator: &T::AccountId) -> Option {
+ >::get(delegator)
+ }
+
+ /// Create and return a new delegation instance.
+ pub(crate) fn new(agent: &T::AccountId, amount: BalanceOf) -> Self {
+ Delegation { agent: agent.clone(), amount }
+ }
+
+ /// Ensure the delegator is either a new delegator or they are adding more delegation to the
+ /// existing agent.
+ ///
+ /// Delegators are prevented from delegating to multiple agents at the same time.
+ pub(crate) fn can_delegate(delegator: &T::AccountId, agent: &T::AccountId) -> bool {
+ Delegation::::get(delegator)
+ .map(|delegation| delegation.agent == *agent)
+ .unwrap_or(
+ // all good if it is a new delegator except it should not be an existing agent.
+ !>::contains_key(delegator),
+ )
+ }
+
+ /// Save self to storage. If the delegation amount is zero, remove the delegation.
+ pub(crate) fn update_or_kill(self, key: &T::AccountId) {
+ // Clean up if no delegation left.
+ if self.amount == Zero::zero() {
+ >::remove(key);
+ return
+ }
+
+ >::insert(key, self)
+ }
+}
+
+/// Ledger of all delegations to an `Agent`.
+///
+/// This keeps track of the active balance of the `Agent` that is made up from the funds that
+/// are currently delegated to this `Agent`. It also tracks the pending slashes yet to be
+/// applied among other things.
+#[derive(Default, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
+#[scale_info(skip_type_params(T))]
+pub struct AgentLedger {
+ /// Where the reward should be paid out.
+ pub payee: T::AccountId,
+ /// Sum of all delegated funds to this `Agent`.
+ #[codec(compact)]
+ pub total_delegated: BalanceOf,
+ /// Funds that are withdrawn from core staking but not released to delegator/s. It is a subset
+ /// of `total_delegated` and can never be greater than it.
+ ///
+ /// We need this register to ensure that the `Agent` does not bond funds from delegated
+ /// funds that are withdrawn and should be claimed by delegators.
+ #[codec(compact)]
+ pub unclaimed_withdrawals: BalanceOf,
+ /// Slashes that are not yet applied. This affects the effective balance of the `Agent`.
+ #[codec(compact)]
+ pub pending_slash: BalanceOf,
+}
+
+impl AgentLedger {
+ /// Create a new instance of `AgentLedger`.
+ pub(crate) fn new(reward_destination: &T::AccountId) -> Self {
+ AgentLedger {
+ payee: reward_destination.clone(),
+ total_delegated: Zero::zero(),
+ unclaimed_withdrawals: Zero::zero(),
+ pending_slash: Zero::zero(),
+ }
+ }
+
+ /// Get `AgentLedger` from storage.
+ pub(crate) fn get(key: &T::AccountId) -> Option {
+ >::get(key)
+ }
+
+ /// Save self to storage with the given key.
+ pub(crate) fn update(self, key: &T::AccountId) {
+ >::insert(key, self)
+ }
+
+ /// Effective total balance of the `Agent`.
+ ///
+ /// This takes into account any slashes reported to `Agent` but unapplied.
+ pub(crate) fn effective_balance(&self) -> BalanceOf {
+ defensive_assert!(
+ self.total_delegated >= self.pending_slash,
+ "slash cannot be higher than actual balance of delegator"
+ );
+
+ // pending slash needs to be burned and cannot be used for stake.
+ self.total_delegated.saturating_sub(self.pending_slash)
+ }
+
+ /// Agent balance that can be staked/bonded in [`T::CoreStaking`].
+ pub(crate) fn stakeable_balance(&self) -> BalanceOf {
+ self.effective_balance().saturating_sub(self.unclaimed_withdrawals)
+ }
+}
+
+/// Wrapper around `AgentLedger` to provide some helper functions to mutate the ledger.
+#[derive(Clone)]
+pub struct Agent {
+ /// storage key
+ pub key: T::AccountId,
+ /// storage value
+ pub ledger: AgentLedger,
+}
+
+impl Agent {
+ /// Get `Agent` from storage if it exists or return an error.
+ pub(crate) fn get(agent: &T::AccountId) -> Result, DispatchError> {
+ let ledger = AgentLedger::::get(agent).ok_or(Error::::NotAgent)?;
+ Ok(Agent { key: agent.clone(), ledger })
+ }
+
+ /// Remove funds that are withdrawn from [Config::CoreStaking] but not claimed by a delegator.
+ ///
+ /// Checked decrease of delegation amount from `total_delegated` and `unclaimed_withdrawals`
+ /// registers. Consumes self and returns a new instance of self if success.
+ pub(crate) fn remove_unclaimed_withdraw(
+ self,
+ amount: BalanceOf,
+ ) -> Result {
+ let new_total_delegated = self
+ .ledger
+ .total_delegated
+ .checked_sub(&amount)
+ .defensive_ok_or(ArithmeticError::Overflow)?;
+ let new_unclaimed_withdrawals = self
+ .ledger
+ .unclaimed_withdrawals
+ .checked_sub(&amount)
+ .defensive_ok_or(ArithmeticError::Overflow)?;
+
+ Ok(Agent {
+ ledger: AgentLedger {
+ total_delegated: new_total_delegated,
+ unclaimed_withdrawals: new_unclaimed_withdrawals,
+ ..self.ledger
+ },
+ ..self
+ })
+ }
+
+ /// Add funds that are withdrawn from [Config::CoreStaking] to be claimed by delegators later.
+ pub(crate) fn add_unclaimed_withdraw(
+ self,
+ amount: BalanceOf,
+ ) -> Result {
+ let new_unclaimed_withdrawals = self
+ .ledger
+ .unclaimed_withdrawals
+ .checked_add(&amount)
+ .defensive_ok_or(ArithmeticError::Overflow)?;
+
+ Ok(Agent {
+ ledger: AgentLedger { unclaimed_withdrawals: new_unclaimed_withdrawals, ..self.ledger },
+ ..self
+ })
+ }
+
+ /// Amount that is delegated but not bonded yet.
+ ///
+ /// This importantly does not include `unclaimed_withdrawals` as those should not be bonded
+ /// again unless explicitly requested.
+ pub(crate) fn available_to_bond(&self) -> BalanceOf {
+ let bonded_stake = self.bonded_stake();
+ let stakeable = self.ledger.stakeable_balance();
+
+ defensive_assert!(
+ stakeable >= bonded_stake,
+ "cannot be bonded with more than total amount delegated to agent"
+ );
+
+ stakeable.saturating_sub(bonded_stake)
+ }
+
+ /// Remove slashes from the `AgentLedger`.
+ pub(crate) fn remove_slash(self, amount: BalanceOf) -> Self {
+ let pending_slash = self.ledger.pending_slash.defensive_saturating_sub(amount);
+ let total_delegated = self.ledger.total_delegated.defensive_saturating_sub(amount);
+
+ Agent { ledger: AgentLedger { pending_slash, total_delegated, ..self.ledger }, ..self }
+ }
+
+ /// Get the total stake of agent bonded in [`Config::CoreStaking`].
+ pub(crate) fn bonded_stake(&self) -> BalanceOf {
+ T::CoreStaking::total_stake(&self.key).unwrap_or(Zero::zero())
+ }
+
+ /// Returns true if the agent is bonded in [`Config::CoreStaking`].
+ pub(crate) fn is_bonded(&self) -> bool {
+ T::CoreStaking::stake(&self.key).is_ok()
+ }
+
+ /// Returns the reward account registered by the agent.
+ pub(crate) fn reward_account(&self) -> &T::AccountId {
+ &self.ledger.payee
+ }
+
+ /// Save self to storage.
+ pub(crate) fn save(self) {
+ let key = self.key;
+ self.ledger.update(&key)
+ }
+
+ /// Save self and remove if no delegation left.
+ ///
+ /// Returns:
+ /// - true if agent killed.
+ /// - error if the delegate is in an unexpected state.
+ pub(crate) fn update_or_kill(self) -> Result {
+ let key = self.key;
+ // see if delegate can be killed
+ if self.ledger.total_delegated == Zero::zero() {
+ ensure!(
+ self.ledger.unclaimed_withdrawals == Zero::zero() &&
+ self.ledger.pending_slash == Zero::zero(),
+ Error::::BadState
+ );
+ >::remove(key);
+ return Ok(true)
+ }
+ self.ledger.update(&key);
+ Ok(false)
+ }
+
+ /// Reloads self from storage.
+ pub(crate) fn refresh(self) -> Result, DispatchError> {
+ Self::get(&self.key)
+ }
+
+ /// Balance of `Agent` that is not bonded.
+ ///
+ /// This is similar to [Self::available_to_bond] except it also includes `unclaimed_withdrawals`
+ /// of `Agent`.
+ #[cfg(test)]
+ #[allow(unused)]
+ pub(crate) fn total_unbonded(&self) -> BalanceOf {
+ let bonded_stake = self.bonded_stake();
+
+ let net_balance = self.ledger.effective_balance();
+
+ assert!(net_balance >= bonded_stake, "cannot be bonded with more than the agent balance");
+
+ net_balance.saturating_sub(bonded_stake)
+ }
+}
diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs
index 692e62acfdff..4f91fd6dff22 100644
--- a/substrate/frame/staking/src/lib.rs
+++ b/substrate/frame/staking/src/lib.rs
@@ -376,7 +376,7 @@ pub struct ActiveEraInfo {
///
/// Start can be none if start hasn't been set for the era yet,
/// Start is set on the first on_finalize of the era to guarantee usage of `Time`.
- start: Option,
+ pub start: Option,
}
/// Reward points of an era. Used to split era total payout between validators.
diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs
index ad6cc6e2f4ff..c7045508cea3 100644
--- a/substrate/primitives/staking/src/lib.rs
+++ b/substrate/primitives/staking/src/lib.rs
@@ -456,4 +456,123 @@ pub struct PagedExposureMetadata {
pub page_count: Page,
}
+/// Trait to provide delegation functionality for stakers.
+///
+/// Introduces two new terms to the staking system:
+/// - `Delegator`: An account that delegates funds to an `Agent`.
+/// - `Agent`: An account that receives delegated funds from `Delegators`. It can then use these
+/// funds to participate in the staking system. It can never use its own funds to stake. They
+/// (virtually bond)[`StakingUnchecked::virtual_bond`] into the staking system and can also be
+/// termed as `Virtual Nominators`.
+///
+/// The `Agent` is responsible for managing rewards and slashing for all the `Delegators` that
+/// have delegated funds to it.
+pub trait DelegationInterface {
+ /// Balance type used by the staking system.
+ type Balance: Sub