From 4d47b4431beaf12ed9ea45c1fb97ae87f0d145cd Mon Sep 17 00:00:00 2001 From: Ankan <10196091+Ank4n@users.noreply.github.com> Date: Wed, 15 May 2024 14:00:24 +0200 Subject: [PATCH] Introduces: Delegated Staking Pallet (#3904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the second PR in preparation for https://github.com/paritytech/polkadot-sdk/issues/454. ## Also see - **Precursor** https://github.com/paritytech/polkadot-sdk/pull/3889. - **Follow up** https://github.com/paritytech/polkadot-sdk/pull/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 Co-authored-by: georgepisaltu <52418509+georgepisaltu@users.noreply.github.com> --- Cargo.lock | 22 + Cargo.toml | 1 + prdoc/pr_3904.prdoc | 19 + substrate/frame/delegated-staking/Cargo.toml | 69 ++ .../frame/delegated-staking/src/impls.rs | 149 ++++ substrate/frame/delegated-staking/src/lib.rs | 815 ++++++++++++++++++ substrate/frame/delegated-staking/src/mock.rs | 308 +++++++ .../frame/delegated-staking/src/tests.rs | 685 +++++++++++++++ .../frame/delegated-staking/src/types.rs | 292 +++++++ substrate/frame/staking/src/lib.rs | 2 +- substrate/primitives/staking/src/lib.rs | 119 +++ 11 files changed, 2480 insertions(+), 1 deletion(-) create mode 100644 prdoc/pr_3904.prdoc create mode 100644 substrate/frame/delegated-staking/Cargo.toml create mode 100644 substrate/frame/delegated-staking/src/impls.rs create mode 100644 substrate/frame/delegated-staking/src/lib.rs create mode 100644 substrate/frame/delegated-staking/src/mock.rs create mode 100644 substrate/frame/delegated-staking/src/tests.rs create mode 100644 substrate/frame/delegated-staking/src/types.rs 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 + + Ord + + PartialEq + + Default + + Copy + + MaxEncodedLen + + FullCodec + + TypeInfo + + Saturating; + + /// AccountId type used by the staking system. + type AccountId: Clone + core::fmt::Debug; + + /// Effective balance of the `Agent` account. + /// + /// This takes into account any pending slashes to `Agent`. + fn agent_balance(agent: &Self::AccountId) -> Self::Balance; + + /// Returns the total amount of funds delegated by a `delegator`. + fn delegator_balance(delegator: &Self::AccountId) -> Self::Balance; + + /// Delegate funds to `Agent`. + /// + /// Only used for the initial delegation. Use [`Self::delegate_extra`] to add more delegation. + fn delegate( + delegator: &Self::AccountId, + agent: &Self::AccountId, + reward_account: &Self::AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Add more delegation to the `Agent`. + /// + /// If this is the first delegation, use [`Self::delegate`] instead. + fn delegate_extra( + delegator: &Self::AccountId, + agent: &Self::AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Withdraw or revoke delegation to `Agent`. + /// + /// If there are `Agent` funds upto `amount` available to withdraw, then those funds would + /// be released to the `delegator` + fn withdraw_delegation( + delegator: &Self::AccountId, + agent: &Self::AccountId, + amount: Self::Balance, + num_slashing_spans: u32, + ) -> DispatchResult; + + /// Returns true if there are pending slashes posted to the `Agent` account. + /// + /// Slashes to `Agent` account are not immediate and are applied lazily. Since `Agent` + /// has an unbounded number of delegators, immediate slashing is not possible. + fn has_pending_slash(agent: &Self::AccountId) -> bool; + + /// Apply a pending slash to an `Agent` by slashing `value` from `delegator`. + /// + /// A reporter may be provided (if one exists) in order for the implementor to reward them, + /// if applicable. + fn delegator_slash( + agent: &Self::AccountId, + delegator: &Self::AccountId, + value: Self::Balance, + maybe_reporter: Option, + ) -> DispatchResult; +} + +/// Trait to provide functionality for direct stakers to migrate to delegation agents. +/// See [`DelegationInterface`] for more details on delegation. +pub trait DelegationMigrator { + /// Balance type used by the staking system. + type Balance: Sub + + Ord + + PartialEq + + Default + + Copy + + MaxEncodedLen + + FullCodec + + TypeInfo + + Saturating; + + /// AccountId type used by the staking system. + type AccountId: Clone + core::fmt::Debug; + + /// Migrate an existing `Nominator` to `Agent` account. + /// + /// The implementation should ensure the `Nominator` account funds are moved to an escrow + /// from which `Agents` can later release funds to its `Delegators`. + fn migrate_nominator_to_agent( + agent: &Self::AccountId, + reward_account: &Self::AccountId, + ) -> DispatchResult; + + /// Migrate `value` of delegation to `delegator` from a migrating agent. + /// + /// When a direct `Nominator` migrates to `Agent`, the funds are kept in escrow. This function + /// allows the `Agent` to release the funds to the `delegator`. + fn migrate_delegation( + agent: &Self::AccountId, + delegator: &Self::AccountId, + value: Self::Balance, + ) -> DispatchResult; +} + sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $);