diff --git a/Cargo.lock b/Cargo.lock
index 413bd28abe03..a87d03b5b1c7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9698,6 +9698,29 @@ dependencies = [
"sp-std 14.0.0",
]
+[[package]]
+name = "pallet-delegated-staking"
+version = "4.0.0-dev"
+dependencies = [
+ "frame-election-provider-support",
+ "frame-support",
+ "frame-system",
+ "pallet-balances",
+ "pallet-nomination-pools",
+ "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"
@@ -19111,6 +19134,7 @@ dependencies = [
"serde",
"sp-core",
"sp-runtime",
+ "sp-std 14.0.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index e6162830375f..2966e45fbec0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -385,6 +385,7 @@ members = [
"substrate/frame/staking/reward-curve",
"substrate/frame/staking/reward-fn",
"substrate/frame/staking/runtime-api",
+ "substrate/frame/delegated-staking",
"substrate/frame/state-trie-migration",
"substrate/frame/statement",
"substrate/frame/sudo",
diff --git a/substrate/frame/delegated-staking/Cargo.toml b/substrate/frame/delegated-staking/Cargo.toml
new file mode 100644
index 000000000000..b4b9768256c6
--- /dev/null
+++ b/substrate/frame/delegated-staking/Cargo.toml
@@ -0,0 +1,70 @@
+[package]
+name = "pallet-delegated-staking"
+version = "4.0.0-dev"
+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-nomination-pools = { path = "../nomination-pools" }
+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-support/std",
+ "frame-system/std",
+ "scale-info/std",
+ "sp-core/std",
+ "sp-io/std",
+ "sp-std/std",
+ "sp-runtime/std",
+ "sp-staking/std",
+ "pallet-balances/std",
+ "pallet-staking/std",
+ "pallet-timestamp/std",
+ "frame-election-provider-support/std",
+]
+runtime-benchmarks = [
+ "frame-support/runtime-benchmarks",
+ "frame-system/runtime-benchmarks",
+ "sp-runtime/runtime-benchmarks",
+ "sp-staking/runtime-benchmarks",
+ "pallet-balances/runtime-benchmarks",
+ "pallet-staking/runtime-benchmarks",
+ "pallet-timestamp/runtime-benchmarks",
+ "frame-election-provider-support/runtime-benchmarks",
+]
+try-runtime = [
+ "frame-support/try-runtime",
+ "frame-system/try-runtime",
+ "sp-runtime/try-runtime",
+ "pallet-balances/try-runtime",
+ "pallet-staking/try-runtime",
+ "pallet-timestamp/try-runtime",
+ "frame-election-provider-support/try-runtime",
+]
diff --git a/substrate/frame/delegated-staking/src/benchmarking.rs b/substrate/frame/delegated-staking/src/benchmarking.rs
new file mode 100644
index 000000000000..808d19a5ce9a
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/benchmarking.rs
@@ -0,0 +1,20 @@
+// 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.
+
+//! Benchmarking for pallet-delegated-staking.
+
+#![cfg(feature = "runtime-benchmarks")]
diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs
new file mode 100644
index 000000000000..e256b917a94b
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/impls.rs
@@ -0,0 +1,288 @@
+// 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 [StakingInterface], [DelegatedStakeInterface] and
+//! [OnStakingUpdate].
+
+use super::*;
+use sp_staking::{delegation::DelegatedStakeInterface, OnStakingUpdate};
+
+/// StakingInterface implementation with delegation support.
+///
+/// Only supports Nominators via Delegated Bonds. It is possible for a nominator to migrate and
+/// become a `delegatee`.
+impl StakingInterface for Pallet {
+ type Balance = BalanceOf;
+ type AccountId = T::AccountId;
+ type CurrencyToVote = ::CurrencyToVote;
+
+ fn minimum_nominator_bond() -> Self::Balance {
+ T::CoreStaking::minimum_nominator_bond()
+ }
+
+ fn minimum_validator_bond() -> Self::Balance {
+ T::CoreStaking::minimum_validator_bond()
+ }
+
+ fn stash_by_ctrl(_controller: &Self::AccountId) -> Result {
+ // ctrl are deprecated, just return err.
+ Err(Error::::NotSupported.into())
+ }
+
+ fn bonding_duration() -> EraIndex {
+ T::CoreStaking::bonding_duration()
+ }
+
+ fn current_era() -> EraIndex {
+ T::CoreStaking::current_era()
+ }
+
+ fn stake(who: &Self::AccountId) -> Result, DispatchError> {
+ ensure!(Self::is_delegatee(who), Error::::NotSupported);
+ return T::CoreStaking::stake(who);
+ }
+
+ fn total_stake(who: &Self::AccountId) -> Result {
+ if Self::is_delegatee(who) {
+ return T::CoreStaking::total_stake(who);
+ }
+
+ if Self::is_delegator(who) {
+ let delegation = Delegation::::get(who).defensive_ok_or(Error::::BadState)?;
+ return Ok(delegation.amount);
+ }
+
+ Err(Error::::NotSupported.into())
+ }
+
+ fn active_stake(who: &Self::AccountId) -> Result {
+ T::CoreStaking::active_stake(who)
+ }
+
+ fn is_unbonding(who: &Self::AccountId) -> Result {
+ T::CoreStaking::is_unbonding(who)
+ }
+
+ fn fully_unbond(who: &Self::AccountId) -> DispatchResult {
+ ensure!(Self::is_delegatee(who), Error::::NotSupported);
+ return T::CoreStaking::fully_unbond(who);
+ }
+
+ fn bond(
+ who: &Self::AccountId,
+ value: Self::Balance,
+ payee: &Self::AccountId,
+ ) -> DispatchResult {
+ // ensure who is not already staked
+ ensure!(T::CoreStaking::status(who).is_err(), Error::::AlreadyStaking);
+ let delegatee = Delegatee::::from(who)?;
+
+ ensure!(delegatee.available_to_bond() >= value, Error::::NotEnoughFunds);
+ ensure!(delegatee.ledger.payee == *payee, Error::::InvalidRewardDestination);
+
+ T::CoreStaking::virtual_bond(who, value, payee)
+ }
+
+ fn nominate(who: &Self::AccountId, validators: Vec) -> DispatchResult {
+ ensure!(Self::is_delegatee(who), Error::::NotDelegatee);
+ return T::CoreStaking::nominate(who, validators);
+ }
+
+ fn chill(who: &Self::AccountId) -> DispatchResult {
+ ensure!(Self::is_delegatee(who), Error::::NotDelegatee);
+ return T::CoreStaking::chill(who);
+ }
+
+ fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult {
+ let ledger = >::get(who).ok_or(Error::::NotDelegatee)?;
+ ensure!(ledger.stakeable_balance() >= extra, Error::::NotEnoughFunds);
+
+ T::CoreStaking::bond_extra(who, extra)
+ }
+
+ fn unbond(stash: &Self::AccountId, value: Self::Balance) -> DispatchResult {
+ let delegatee = Delegatee::::from(stash)?;
+ ensure!(delegatee.bonded_stake() >= value, Error::::NotEnoughFunds);
+
+ T::CoreStaking::unbond(stash, value)
+ }
+
+ fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult {
+ T::CoreStaking::update_payee(stash, reward_acc)
+ }
+
+ /// Withdraw unbonding funds until current era.
+ ///
+ /// Funds are moved to unclaimed_withdrawals register of the `DelegateeLedger`.
+ fn withdraw_unbonded(
+ delegatee_acc: Self::AccountId,
+ num_slashing_spans: u32,
+ ) -> Result {
+ Pallet::::withdraw_unbonded(&delegatee_acc, num_slashing_spans)
+ .map(|delegatee| delegatee.ledger.total_delegated.is_zero())
+ }
+
+ fn desired_validator_count() -> u32 {
+ T::CoreStaking::desired_validator_count()
+ }
+
+ fn election_ongoing() -> bool {
+ T::CoreStaking::election_ongoing()
+ }
+
+ fn force_unstake(_who: Self::AccountId) -> DispatchResult {
+ Err(Error::::NotSupported.into())
+ }
+
+ fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool {
+ T::CoreStaking::is_exposed_in_era(who, era)
+ }
+
+ fn status(who: &Self::AccountId) -> Result, DispatchError> {
+ ensure!(Self::is_delegatee(who), Error::::NotDelegatee);
+ T::CoreStaking::status(who)
+ }
+
+ fn is_validator(who: &Self::AccountId) -> bool {
+ T::CoreStaking::is_validator(who)
+ }
+
+ fn nominations(who: &Self::AccountId) -> Option> {
+ T::CoreStaking::nominations(who)
+ }
+
+ fn slash_reward_fraction() -> Perbill {
+ T::CoreStaking::slash_reward_fraction()
+ }
+
+ #[cfg(feature = "runtime-benchmarks")]
+ fn max_exposure_page_size() -> sp_staking::Page {
+ T::CoreStaking::max_exposure_page_size()
+ }
+
+ #[cfg(feature = "runtime-benchmarks")]
+ fn add_era_stakers(
+ current_era: &EraIndex,
+ stash: &Self::AccountId,
+ exposures: Vec<(Self::AccountId, Self::Balance)>,
+ ) {
+ T::CoreStaking::add_era_stakers(current_era, stash, exposures)
+ }
+
+ #[cfg(feature = "runtime-benchmarks")]
+ fn set_current_era(era: EraIndex) {
+ T::CoreStaking::set_current_era(era)
+ }
+}
+
+impl DelegatedStakeInterface for Pallet {
+ /// Effective balance of the delegatee account.
+ fn delegatee_balance(who: &Self::AccountId) -> Self::Balance {
+ Delegatee::::from(who)
+ .map(|delegatee| delegatee.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 `Delegatee`.
+ fn delegate(
+ who: &Self::AccountId,
+ delegatee: &Self::AccountId,
+ reward_account: &Self::AccountId,
+ amount: Self::Balance,
+ ) -> DispatchResult {
+ Pallet::::register_as_delegatee(
+ RawOrigin::Signed(delegatee.clone()).into(),
+ reward_account.clone(),
+ )?;
+
+ // Delegate the funds from who to the delegatee account.
+ Pallet::::delegate_funds(
+ RawOrigin::Signed(who.clone()).into(),
+ delegatee.clone(),
+ amount,
+ )
+ }
+
+ /// Add more delegation to the delegatee account.
+ fn delegate_extra(
+ who: &Self::AccountId,
+ delegatee: &Self::AccountId,
+ amount: Self::Balance,
+ ) -> DispatchResult {
+ Pallet::::delegate_funds(
+ RawOrigin::Signed(who.clone()).into(),
+ delegatee.clone(),
+ amount,
+ )
+ }
+
+ /// Withdraw delegation of `delegator` to `delegatee`.
+ ///
+ /// If there are funds in `delegatee` account that can be withdrawn, then those funds would be
+ /// unlocked/released in the delegator's account.
+ fn withdraw_delegation(
+ delegator: &Self::AccountId,
+ delegatee: &Self::AccountId,
+ amount: Self::Balance,
+ ) -> DispatchResult {
+ // fixme(ank4n): Can this not require slashing spans?
+ Pallet::::release(
+ RawOrigin::Signed(delegatee.clone()).into(),
+ delegator.clone(),
+ amount,
+ 0,
+ )
+ }
+
+ /// Returns true if the `delegatee` have any slash pending to be applied.
+ fn has_pending_slash(delegatee: &Self::AccountId) -> bool {
+ Delegatee::::from(delegatee)
+ .map(|d| !d.ledger.pending_slash.is_zero())
+ .unwrap_or(false)
+ }
+
+ fn delegator_slash(
+ delegatee: &Self::AccountId,
+ delegator: &Self::AccountId,
+ value: Self::Balance,
+ maybe_reporter: Option,
+ ) -> sp_runtime::DispatchResult {
+ Pallet::::do_slash(delegatee.clone(), delegator.clone(), value, maybe_reporter)
+ }
+}
+
+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 delegatee, register the slashed amount as pending slash.
+ Some(register) => register.pending_slash.saturating_accrue(slashed_total),
+ None => {
+ // nothing to do
+ },
+ });
+ }
+}
diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs
new file mode 100644
index 000000000000..3b49ff477f27
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/lib.rs
@@ -0,0 +1,827 @@
+// 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
+//!
+//! An abstraction over staking pallet to support delegation of funds to a `delegatee` account which
+//! can use all the delegated funds to it in the staking pallet as if its own fund.
+//!
+//! NOTE: The pallet exposes some dispatchable calls already, but they might not be fully usable
+//! from outside the runtime. In the current version, the pallet is meant to be used by other
+//! pallets in the same runtime. Eventually though, expect those calls to be functionally complete
+//! and usable by off-chain programs as well as xcm based multi locations.
+//!
+//! Declaring dispatchable still has the benefit of being transactable for unit tests as well as
+//! aligned with general direction of moving towards a permissionless pallet. For example, we could
+//! clearly signal who is the expected signer of any interaction with this pallet and take into
+//! account any security considerations associated with those interactions.
+//!
+//! ## 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 account for the intent of staking.
+//!
+//! #### Reward and Slashing
+//! This pallet does not enforce any specific strategy for how rewards or slashes are applied. It
+//! is upto the `delegatee` 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, a `delegatee` account can choose to first slash the reward pot before slashing the
+//! delegators. Or part of the reward can go to a 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`.
+//!
+//! ## Key Terminologies
+//! - **Delegatee**: An account who accepts delegations from other accounts.
+//! - **Delegator**: An account who delegates their funds to a `delegatee`.
+//! - **DelegateeLedger**: A data structure that stores important information about the `delegatee`
+//! such as their total delegated stake.
+//! - **Delegation**: A data structure that stores the amount of funds delegated to a `delegatee` by
+//! a `delegator`.
+//!
+//! ## Interface
+//!
+//! #### Dispatchable Calls
+//! The pallet exposes the following [`Call`]s:
+//! - `register_as_delegatee`: Register an account to be a `delegatee`. Once an account is
+//! registered as a `delegatee`, for staking operations, only its delegated funds are used. This
+//! means it cannot use its own free balance to stake.
+//! - `migrate_to_delegate`: This allows a `Nominator` account to become a `delegatee` account.
+//! Explained in more detail in the `Migration` section.
+//! - `release`: Release funds to `delegator` from `unclaimed_withdrawals` register of the
+//! `delegatee`.
+//! - `migrate_delegation`: Migrate delegated funds from one account to another. This is useful for
+//! example, delegators to a pool account which has migrated to be `delegatee` to migrate their
+//! funds from pool account back to their own account and delegated to pool as a `delegator`. Once
+//! the funds are migrated, the `delegator` can use the funds for other purposes which allows
+//! usage of held funds in an account, such as governance.
+//! - `delegate_funds`: Delegate funds to a `delegatee` account and update the bond to staking.
+//! - `apply_slash`: If there is a pending slash in `delegatee` ledger, the passed delegator's
+//! balance is slashed by the amount and the slash is removed from the delegatee ledger.
+//!
+//! #### [Staking Interface](StakingInterface)
+//! This pallet reimplements the staking interface as a wrapper implementation over
+//! [Config::CoreStaking] to provide delegation based staking. NominationPool can use this pallet as
+//! its Staking provider to support delegation based staking from pool accounts.
+//!
+//! ## 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 a `delegatee` are posted in its
+//! `DelegateeLedger` 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 `delegatee`'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 pending
+//! slashes are applied.
+//!
+//! The user of this pallet can apply slash using
+//! [StakingInterface::delegator_slash](sp_staking::StakingInterface::delegator_slash).
+//!
+//! ## Migration from Nominator to Delegatee
+//! More details [here](https://hackmd.io/@ak0n/np-delegated-staking-migration).
+//!
+//! ## Nomination Pool vs Delegation Staking
+//! This pallet is not a replacement for Nomination Pool but adds a new primitive over staking
+//! pallet that can be used by Nomination Pool to support delegation based staking. It can be
+//! thought of as something in middle of Nomination Pool and Staking Pallet. 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. This is completely configurable and a 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 a `Delegatee` 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)]
+
+#[cfg(test)]
+mod mock;
+#[cfg(test)]
+mod tests;
+
+pub use pallet::*;
+
+mod types;
+
+use types::*;
+
+mod impls;
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+
+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,
+ },
+ weights::Weight,
+};
+
+use sp_runtime::{
+ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero},
+ ArithmeticError, DispatchResult, Perbill, RuntimeDebug, Saturating,
+};
+use sp_staking::{EraIndex, Stake, StakerStatus, StakingInterface, StakingUnsafe};
+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>;
+
+ /// Overarching hold reason.
+ type RuntimeHoldReason: From;
+
+ /// Core staking implementation.
+ type CoreStaking: StakingUnsafe, 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 `delegatee` 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 delegatee account.
+ NotDelegatee,
+ /// Not a Delegator account.
+ NotDelegator,
+ /// Some corruption in internal state.
+ BadState,
+ /// Unapplied pending slash restricts operation on `delegatee`.
+ UnappliedSlash,
+ /// 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)]
+ Delegating,
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub (super) fn deposit_event)]
+ pub enum Event {
+ /// Funds delegated by a delegator.
+ Delegated { delegatee: T::AccountId, delegator: T::AccountId, amount: BalanceOf },
+ /// Funds released to a delegator.
+ Released { delegatee: T::AccountId, delegator: T::AccountId, amount: BalanceOf },
+ /// Funds slashed from a delegator.
+ Slashed { delegatee: 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 `delegatee` 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 `Delegatee` to their `DelegateeLedger`.
+ #[pallet::storage]
+ pub(crate) type Delegatees =
+ CountedStorageMap<_, Twox64Concat, T::AccountId, DelegateeLedger, OptionQuery>;
+
+ #[pallet::call]
+ impl Pallet {
+ /// Register an account to be a `Delegatee`.
+ ///
+ /// `Delegatee` accounts accepts delegations from other `delegator`s and stake funds on
+ /// their behalf.
+ #[pallet::call_index(0)]
+ #[pallet::weight(Weight::default())]
+ pub fn register_as_delegatee(
+ origin: OriginFor,
+ reward_account: T::AccountId,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+
+ // Existing `delegatee` cannot register again.
+ ensure!(!Self::is_delegatee(&who), Error::::NotAllowed);
+
+ // A delegator cannot become a `delegatee`.
+ ensure!(!Self::is_delegator(&who), Error::::NotAllowed);
+
+ // They cannot be already a direct staker in the staking pallet.
+ ensure!(Self::not_direct_staker(&who), Error::::AlreadyStaking);
+
+ // Reward account cannot be same as `delegatee` account.
+ ensure!(reward_account != who, Error::::InvalidRewardDestination);
+
+ Self::do_register_delegatee(&who, &reward_account);
+ Ok(())
+ }
+
+ /// Migrate from a `Nominator` account to `Delegatee` account.
+ ///
+ /// The origin needs to
+ /// - be a `Nominator` with `CoreStaking`,
+ /// - not already a `Delegatee`,
+ /// - have enough funds to transfer existential deposit to a delegator account created for
+ /// the migration.
+ ///
+ /// This operation will create a new delegator account for the origin called
+ /// `proxy_delegator` and transfer the staked amount to it. The `proxy_delegator` delegates
+ /// the funds to the origin making origin a `Delegatee` account. The actual `delegator`
+ /// accounts of the origin can later migrate their funds using [Call::migrate_delegation] to
+ /// claim back their share of delegated funds from `proxy_delegator` to self.
+ #[pallet::call_index(1)]
+ #[pallet::weight(Weight::default())]
+ pub fn migrate_to_delegatee(
+ origin: OriginFor,
+ reward_account: T::AccountId,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+ // ensure who is not already a delegatee.
+ ensure!(!Self::is_delegatee(&who), Error::::NotAllowed);
+
+ // and they should already be a nominator in `CoreStaking`.
+ ensure!(Self::is_direct_nominator(&who), Error::::NotAllowed);
+
+ // Reward account cannot be same as `delegatee` account.
+ ensure!(reward_account != who, Error::::InvalidRewardDestination);
+
+ Self::do_migrate_to_delegatee(&who, &reward_account)
+ }
+
+ /// Release delegated amount to delegator.
+ ///
+ /// This can be called by existing `delegatee` accounts.
+ ///
+ /// Tries to withdraw unbonded fund from `CoreStaking` if needed and release amount to
+ /// `delegator`.
+ #[pallet::call_index(2)]
+ #[pallet::weight(Weight::default())]
+ pub fn release(
+ 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 fund.
+ ///
+ /// This can be called by migrating `delegatee` accounts.
+ ///
+ /// This moves delegator funds from `pxoxy_delegator` account to `delegator` account.
+ #[pallet::call_index(3)]
+ #[pallet::weight(Weight::default())]
+ pub fn migrate_delegation(
+ origin: OriginFor,
+ delegator: T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let delegatee = ensure_signed(origin)?;
+
+ // Ensure they have minimum delegation.
+ ensure!(amount >= T::Currency::minimum_balance(), Error::::NotEnoughFunds);
+
+ // Ensure delegator is sane.
+ ensure!(!Self::is_delegatee(&delegator), Error::::NotAllowed);
+ ensure!(!Self::is_delegator(&delegator), Error::::NotAllowed);
+ ensure!(Self::not_direct_staker(&delegator), Error::::AlreadyStaking);
+
+ // ensure delegatee is sane.
+ ensure!(Self::is_delegatee(&delegatee), Error::::NotDelegatee);
+
+ // and has enough delegated balance to migrate.
+ let proxy_delegator = Self::sub_account(AccountType::ProxyDelegator, delegatee);
+ let balance_remaining = Self::held_balance_of(&proxy_delegator);
+ ensure!(balance_remaining >= amount, Error::::NotEnoughFunds);
+
+ Self::do_migrate_delegation(&proxy_delegator, &delegator, amount)
+ }
+
+ /// Delegate funds to a `Delegatee` account and bonds it to [Config::CoreStaking].
+ ///
+ /// If delegation already exists, it increases the delegation by `amount`.
+ #[pallet::call_index(4)]
+ #[pallet::weight(Weight::default())]
+ pub fn delegate_funds(
+ origin: OriginFor,
+ delegatee: T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+
+ // ensure amount is over minimum to delegate
+ ensure!(amount > T::Currency::minimum_balance(), Error::::NotEnoughFunds);
+
+ // ensure delegator is sane.
+ ensure!(Delegation::::can_delegate(&who, &delegatee), Error::::InvalidDelegation);
+ ensure!(Self::not_direct_staker(&who), Error::::AlreadyStaking);
+
+ // ensure delegatee is sane.
+ ensure!(Self::is_delegatee(&delegatee), Error::::NotDelegatee);
+
+ let delegator_balance =
+ T::Currency::reducible_balance(&who, Preservation::Preserve, Fortitude::Polite);
+ ensure!(delegator_balance >= amount, Error::::NotEnoughFunds);
+
+ // add to delegation
+ Self::do_delegate(&who, &delegatee, amount)?;
+ // bond the amount to `CoreStaking`.
+ Self::do_bond(&delegatee, amount)
+ }
+
+ /// Apply slash to a delegator account.
+ ///
+ /// `Delegatee` accounts with pending slash in their ledger can call this to apply slash to
+ /// one of its `delegator` account. Each slash to a delegator account needs to be posted
+ /// separately until all pending slash is cleared.
+ #[pallet::call_index(5)]
+ #[pallet::weight(Weight::default())]
+ pub fn apply_slash(
+ origin: OriginFor,
+ delegator: T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+ Self::do_slash(who, delegator, amount, None)
+ }
+ }
+
+ #[pallet::hooks]
+ impl Hooks> for Pallet {
+ #[cfg(feature = "try-runtime")]
+ fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> {
+ Self::do_try_state()
+ }
+
+ fn integrity_test() {}
+ }
+}
+
+impl Pallet {
+ /// Derive a (keyless) pot account from the given delegatee account and account type.
+ pub(crate) fn sub_account(
+ account_type: AccountType,
+ delegatee_account: T::AccountId,
+ ) -> T::AccountId {
+ T::PalletId::get().into_sub_account_truncating((account_type, delegatee_account.clone()))
+ }
+
+ /// Balance of a delegator that is delegated.
+ pub(crate) fn held_balance_of(who: &T::AccountId) -> BalanceOf {
+ T::Currency::balance_on_hold(&HoldReason::Delegating.into(), who)
+ }
+
+ /// Returns true if who is registered as a `Delegatee`.
+ fn is_delegatee(who: &T::AccountId) -> bool {
+ >::contains_key(who)
+ }
+
+ /// Returns true if who is delegating to a `Delegatee` account.
+ fn is_delegator(who: &T::AccountId) -> bool {
+ >::contains_key(who)
+ }
+
+ /// Returns true if who is not already staking on [`Config::CoreStaking`].
+ fn not_direct_staker(who: &T::AccountId) -> bool {
+ T::CoreStaking::status(&who).is_err()
+ }
+
+ /// Returns true if who is a [`StakerStatus::Nominator`] on [`Config::CoreStaking`].
+ fn is_direct_nominator(who: &T::AccountId) -> bool {
+ T::CoreStaking::status(who)
+ .map(|status| matches!(status, StakerStatus::Nominator(_)))
+ .unwrap_or(false)
+ }
+
+ fn do_register_delegatee(who: &T::AccountId, reward_account: &T::AccountId) {
+ DelegateeLedger::::new(reward_account).save(who);
+
+ // Delegatee is a virtual account. Make this account exist.
+ // TODO: Someday if we expose these calls in a runtime, we should take a deposit for
+ // being a delegator.
+ frame_system::Pallet::::inc_providers(who);
+ }
+
+ fn do_migrate_to_delegatee(
+ who: &T::AccountId,
+ reward_account: &T::AccountId,
+ ) -> DispatchResult {
+ // 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());
+
+ // Transfer minimum balance to proxy delegator.
+ T::Currency::transfer(
+ who,
+ &proxy_delegator,
+ T::Currency::minimum_balance(),
+ Preservation::Protect,
+ )
+ .map_err(|_| Error::::NotEnoughFunds)?;
+
+ // Get current stake
+ let stake = T::CoreStaking::stake(who)?;
+
+ // release funds from core staking.
+ T::CoreStaking::force_release(who);
+
+ // transferring just released staked amount. This should never fail but if it does, it
+ // indicates bad state and we abort.
+ T::Currency::transfer(who, &proxy_delegator, stake.total, Preservation::Protect)
+ .map_err(|_| Error::::BadState)?;
+
+ Self::do_register_delegatee(who, reward_account);
+ T::CoreStaking::update_payee(who, reward_account)?;
+
+ Self::do_delegate(&proxy_delegator, who, stake.total)
+ }
+
+ fn do_bond(delegatee_acc: &T::AccountId, amount: BalanceOf) -> DispatchResult {
+ let delegatee = Delegatee::::from(delegatee_acc)?;
+
+ let available_to_bond = delegatee.available_to_bond();
+ defensive_assert!(amount == available_to_bond, "not expected value to bond");
+
+ if delegatee.is_bonded() {
+ T::CoreStaking::bond_extra(&delegatee.key, amount)
+ } else {
+ T::CoreStaking::virtual_bond(&delegatee.key, amount, &delegatee.reward_account())
+ }
+ }
+
+ fn do_delegate(
+ delegator: &T::AccountId,
+ delegatee: &T::AccountId,
+ amount: BalanceOf,
+ ) -> DispatchResult {
+ let mut ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotDelegatee)?;
+
+ let new_delegation_amount =
+ if let Some(existing_delegation) = Delegation::::get(delegator) {
+ ensure!(&existing_delegation.delegatee == delegatee, Error::::InvalidDelegation);
+ existing_delegation
+ .amount
+ .checked_add(&amount)
+ .ok_or(ArithmeticError::Overflow)?
+ } else {
+ amount
+ };
+
+ Delegation::::from(delegatee, new_delegation_amount).save_or_kill(delegator);
+ ledger.total_delegated =
+ ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
+ ledger.save(delegatee);
+
+ T::Currency::hold(&HoldReason::Delegating.into(), delegator, amount)?;
+
+ Self::deposit_event(Event::::Delegated {
+ delegatee: delegatee.clone(),
+ delegator: delegator.clone(),
+ amount,
+ });
+
+ Ok(())
+ }
+
+ fn do_release(
+ who: &T::AccountId,
+ delegator: &T::AccountId,
+ amount: BalanceOf,
+ num_slashing_spans: u32,
+ ) -> DispatchResult {
+ let mut delegatee = Delegatee::::from(who)?;
+ let mut delegation = Delegation::::get(delegator).ok_or(Error::::NotDelegator)?;
+
+ // make sure delegation to be released is sound.
+ ensure!(&delegation.delegatee == who, Error::::NotDelegatee);
+ ensure!(delegation.amount >= amount, Error::::NotEnoughFunds);
+
+ // if we do not already have enough funds to be claimed, try withdraw some more.
+ if delegatee.ledger.unclaimed_withdrawals < amount {
+ // get the updated delegatee
+ delegatee = Self::withdraw_unbonded(who, num_slashing_spans)?;
+ }
+
+ // if we still do not have enough funds to release, abort.
+ ensure!(delegatee.ledger.unclaimed_withdrawals >= amount, Error::::NotEnoughFunds);
+
+ // claim withdraw from delegatee.
+ delegatee.remove_unclaimed_withdraw(amount)?.save_or_kill()?;
+
+ // book keep delegation
+ delegation.amount = delegation
+ .amount
+ .checked_sub(&amount)
+ .defensive_ok_or(ArithmeticError::Overflow)?;
+
+ // remove delegator if nothing delegated anymore
+ delegation.save_or_kill(delegator);
+
+ let released = T::Currency::release(
+ &HoldReason::Delegating.into(),
+ &delegator,
+ amount,
+ Precision::BestEffort,
+ )?;
+
+ defensive_assert!(released == amount, "hold should have been released fully");
+
+ Self::deposit_event(Event::::Released {
+ delegatee: who.clone(),
+ delegator: delegator.clone(),
+ amount,
+ });
+
+ Ok(())
+ }
+
+ fn withdraw_unbonded(
+ delegatee_acc: &T::AccountId,
+ num_slashing_spans: u32,
+ ) -> Result, DispatchError> {
+ let delegatee = Delegatee::::from(delegatee_acc)?;
+ let pre_total = T::CoreStaking::stake(delegatee_acc).defensive()?.total;
+
+ let stash_killed: bool =
+ T::CoreStaking::withdraw_unbonded(delegatee_acc.clone(), num_slashing_spans)
+ .map_err(|_| Error::::WithdrawFailed)?;
+
+ let maybe_post_total = T::CoreStaking::stake(delegatee_acc);
+ // One of them should be true
+ defensive_assert!(
+ !(stash_killed && maybe_post_total.is_ok()),
+ "something horrible happened while withdrawing"
+ );
+
+ let post_total = maybe_post_total.map_or(Zero::zero(), |s| s.total);
+
+ let new_withdrawn =
+ pre_total.checked_sub(&post_total).defensive_ok_or(Error::::BadState)?;
+
+ let delegatee = delegatee.add_unclaimed_withdraw(new_withdrawn)?;
+
+ delegatee.clone().save();
+
+ Ok(delegatee)
+ }
+
+ /// 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 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_delegatee(destination_delegator)
+ );
+
+ // update delegations
+ Delegation::::from(&source_delegation.delegatee, amount)
+ .save_or_kill(destination_delegator);
+
+ source_delegation
+ .decrease_delegation(amount)
+ .defensive_ok_or(Error::::BadState)?
+ .save_or_kill(source_delegator);
+
+ // release funds from source
+ let released = T::Currency::release(
+ &HoldReason::Delegating.into(),
+ &source_delegator,
+ amount,
+ Precision::BestEffort,
+ )?;
+
+ defensive_assert!(released == amount, "hold should have been released fully");
+
+ // transfer the released amount to `destination_delegator`.
+ // Note: The source should have been funded ED in the beginning so it should not be dusted.
+ T::Currency::transfer(
+ &source_delegator,
+ destination_delegator,
+ amount,
+ Preservation::Preserve,
+ )
+ .map_err(|_| Error::::BadState)?;
+
+ // hold the funds again in the new delegator account.
+ T::Currency::hold(&HoldReason::Delegating.into(), &destination_delegator, amount)?;
+
+ Ok(())
+ }
+
+ pub fn do_slash(
+ delegatee_acc: T::AccountId,
+ delegator: T::AccountId,
+ amount: BalanceOf,
+ maybe_reporter: Option,
+ ) -> DispatchResult {
+ let delegatee = Delegatee::::from(&delegatee_acc)?;
+ let delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?;
+
+ ensure!(delegation.delegatee == delegatee_acc, Error::::NotDelegatee);
+ ensure!(delegation.amount >= amount, Error::::NotEnoughFunds);
+
+ let (mut credit, missing) =
+ T::Currency::slash(&HoldReason::Delegating.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 delegatee.
+ delegatee.remove_slash(actual_slash).save();
+
+ delegation
+ .decrease_delegation(actual_slash)
+ .ok_or(ArithmeticError::Overflow)?
+ .save_or_kill(&delegator);
+
+ if let Some(reporter) = maybe_reporter {
+ let reward_payout: BalanceOf =
+ T::CoreStaking::slash_reward_fraction() * actual_slash;
+ let (reporter_reward, rest) = credit.split(reward_payout);
+ credit = rest;
+
+ // fixme(ank4n): handle error
+ let _ = T::Currency::resolve(&reporter, reporter_reward);
+ }
+
+ T::OnSlash::on_unbalanced(credit);
+
+ Self::deposit_event(Event::::Slashed { delegatee: delegatee_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 {
+ Delegatee::::from(who)
+ .map(|delegatee| delegatee.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 = Delegatees::::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 (delegatee, ledger) in ledgers {
+ ensure!(
+ matches!(
+ T::CoreStaking::status(&delegatee).expect("delegatee should be bonded"),
+ StakerStatus::Nominator(_) | StakerStatus::Idle
+ ),
+ "delegatee should be bonded and not validator"
+ );
+
+ ensure!(
+ ledger.stakeable_balance() >=
+ T::CoreStaking::total_stake(&delegatee)
+ .expect("delegatee 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_delegatee(delegator), "delegator cannot be delegatee");
+
+ delegation_aggregation
+ .entry(delegation.delegatee.clone())
+ .and_modify(|e| *e += delegation.amount)
+ .or_insert(delegation.amount);
+ }
+
+ for (delegatee, total_delegated) in delegation_aggregation {
+ ensure!(!Self::is_delegator(&delegatee), "delegatee cannot be delegator");
+
+ let ledger = ledger.get(&delegatee).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..ae1b84acdecb
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/mock.rs
@@ -0,0 +1,359 @@
+// 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::Delegatee, HoldReason};
+use frame_support::{
+ assert_ok, derive_impl,
+ pallet_prelude::*,
+ parameter_types,
+ traits::{fungible::InspectHold, 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::CurrentEra;
+use sp_core::U256;
+use sp_runtime::traits::Convert;
+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 = ();
+ 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 OffendingValidatorsThreshold = ();
+ 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<32>;
+ type MaxControllersInDeprecationBatch = ConstU32<100>;
+ type EventListeners = (Pools, DelegatedStaking);
+ type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
+ type WeightInfo = ();
+}
+
+parameter_types! {
+ pub const DelegatedStakingPalletId: PalletId = PalletId(*b"py/dlstk");
+}
+impl delegated_staking::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type PalletId = DelegatedStakingPalletId;
+ type Currency = Balances;
+ type OnSlash = ();
+ type RuntimeHoldReason = RuntimeHoldReason;
+ type CoreStaking = Staking;
+}
+
+pub struct BalanceToU256;
+impl Convert for BalanceToU256 {
+ fn convert(n: Balance) -> U256 {
+ n.into()
+ }
+}
+pub struct U256ToBalance;
+impl Convert for U256ToBalance {
+ fn convert(n: U256) -> Balance {
+ n.try_into().unwrap()
+ }
+}
+
+parameter_types! {
+ pub static MaxUnbonding: u32 = 8;
+ pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls");
+}
+impl pallet_nomination_pools::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type WeightInfo = ();
+ type Currency = Balances;
+ type RuntimeFreezeReason = RuntimeFreezeReason;
+ type RewardCounter = sp_runtime::FixedU128;
+ type BalanceToU256 = BalanceToU256;
+ type U256ToBalance = U256ToBalance;
+ type PostUnbondingPoolsWindow = ConstU32<2>;
+ type PalletId = PoolsPalletId;
+ type MaxMetadataLen = ConstU32<256>;
+ type MaxUnbonding = MaxUnbonding;
+ type MaxPointsToBalance = frame_support::traits::ConstU8<10>;
+ type StakeAdapter = pallet_nomination_pools::adapter::DelegateStake;
+}
+
+frame_support::construct_runtime!(
+ pub enum Runtime {
+ System: frame_system,
+ Timestamp: pallet_timestamp,
+ Balances: pallet_balances,
+ Staking: pallet_staking,
+ DelegatedStaking: delegated_staking,
+ Pools: pallet_nomination_pools,
+ }
+);
+
+pub struct ExtBuilder {}
+
+impl Default for ExtBuilder {
+ fn default() -> Self {
+ Self {}
+ }
+}
+
+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);
+ });
+
+ 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(|| {
+ // make sure pool state is correct.
+ Pools::do_try_state(255).unwrap();
+ 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(
+ delegatee: AccountId,
+ reward_acc: AccountId,
+ delegators: Vec,
+ base_delegate_amount: Balance,
+ increment: Balance,
+) -> Balance {
+ fund(&delegatee, 100);
+ assert_ok!(DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(delegatee).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_funds(
+ RawOrigin::Signed(delegator.clone()).into(),
+ delegatee,
+ amount_to_delegate
+ ));
+ }
+
+ // sanity checks
+ assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), delegated_amount);
+ assert_eq!(Delegatee::::from(&delegatee).unwrap().available_to_bond(), 0);
+
+ delegated_amount
+}
+
+pub(crate) fn start_era(era: sp_staking::EraIndex) {
+ CurrentEra::::set(Some(era));
+}
+
+pub(crate) fn eq_stake(who: AccountId, total: Balance, active: Balance) -> bool {
+ Staking::stake(&who).unwrap() == Stake { total, active } &&
+ get_delegatee(&who).ledger.stakeable_balance() == total
+}
+
+pub(crate) fn get_delegatee(delegatee: &AccountId) -> Delegatee {
+ Delegatee::::from(delegatee).expect("delegate should exist")
+}
+
+pub(crate) fn held_balance(who: &AccountId) -> Balance {
+ Balances::balance_on_hold(&HoldReason::Delegating.into(), &who)
+}
+
+parameter_types! {
+ static ObservedEventsPools: usize = 0;
+ static ObservedEventsDelegatedStaking: usize = 0;
+}
+
+pub(crate) fn pool_events_since_last_call() -> Vec> {
+ let events = System::events()
+ .into_iter()
+ .map(|r| r.event)
+ .filter_map(|e| if let RuntimeEvent::Pools(inner) = e { Some(inner) } else { None })
+ .collect::>();
+ let already_seen = ObservedEventsPools::get();
+ ObservedEventsPools::set(events.len());
+ events.into_iter().skip(already_seen).collect()
+}
+
+pub(crate) fn events_since_last_call() -> Vec> {
+ let events = System::events()
+ .into_iter()
+ .map(|r| r.event)
+ .filter_map(
+ |e| if let RuntimeEvent::DelegatedStaking(inner) = e { Some(inner) } else { None },
+ )
+ .collect::>();
+ 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..34f8399f340d
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/tests.rs
@@ -0,0 +1,1040 @@
+// 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_nomination_pools::{Error as PoolsError, Event as PoolsEvent};
+use pallet_staking::Error as StakingError;
+
+#[test]
+fn create_a_delegatee_with_first_delegator() {
+ ExtBuilder::default().build_and_execute(|| {
+ let delegatee: AccountId = 200;
+ let reward_account: AccountId = 201;
+ let delegator: AccountId = 202;
+
+ // set intention to accept delegation.
+ fund(&delegatee, 1000);
+ assert_ok!(DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(delegatee).into(),
+ reward_account
+ ));
+
+ // delegate to this account
+ fund(&delegator, 1000);
+ assert_ok!(DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegator).into(),
+ delegatee,
+ 100
+ ));
+
+ // verify
+ assert!(DelegatedStaking::is_delegatee(&delegatee));
+ assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 100);
+ assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator), 100);
+ });
+}
+
+#[test]
+fn cannot_become_delegatee() {
+ ExtBuilder::default().build_and_execute(|| {
+ // cannot set reward account same as delegatee account
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(RawOrigin::Signed(100).into(), 100),
+ Error::::InvalidRewardDestination
+ );
+
+ // an existing validator cannot become delegatee
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(mock::GENESIS_VALIDATOR).into(),
+ 100
+ ),
+ Error::::AlreadyStaking
+ );
+
+ // an existing nominator cannot become delegatee
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(mock::GENESIS_NOMINATOR_ONE).into(),
+ 100
+ ),
+ Error::::AlreadyStaking
+ );
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(mock::GENESIS_NOMINATOR_TWO).into(),
+ 100
+ ),
+ Error::::AlreadyStaking
+ );
+ });
+}
+
+#[test]
+fn create_multiple_delegators() {
+ ExtBuilder::default().build_and_execute(|| {
+ let delegatee: AccountId = 200;
+ let reward_account: AccountId = 201;
+
+ // stakeable balance is 0 for non delegatee
+ fund(&delegatee, 1000);
+ assert!(!DelegatedStaking::is_delegatee(&delegatee));
+ assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 0);
+
+ // set intention to accept delegation.
+ assert_ok!(DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(delegatee).into(),
+ reward_account
+ ));
+
+ // create 100 delegators
+ for i in 202..302 {
+ fund(&i, 100 + ExistentialDeposit::get());
+ assert_ok!(DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(i).into(),
+ delegatee,
+ 100
+ ));
+ // Balance of 100 held on delegator account for delegating to the delegatee.
+ assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &i), 100);
+ }
+
+ // verify
+ assert!(DelegatedStaking::is_delegatee(&delegatee));
+ assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 100 * 100);
+ });
+}
+
+#[test]
+fn delegatee_restrictions() {
+ // Similar to creating a nomination pool
+ ExtBuilder::default().build_and_execute(|| {
+ let delegatee_one = 200;
+ let delegator_one = 210;
+ fund(&delegatee_one, 100);
+ assert_ok!(DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(delegatee_one).into(),
+ delegatee_one + 1
+ ));
+ fund(&delegator_one, 200);
+ assert_ok!(DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegator_one).into(),
+ delegatee_one,
+ 100
+ ));
+
+ let delegatee_two = 300;
+ let delegator_two = 310;
+ fund(&delegatee_two, 100);
+ assert_ok!(DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(delegatee_two).into(),
+ delegatee_two + 1
+ ));
+ fund(&delegator_two, 200);
+ assert_ok!(DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegator_two).into(),
+ delegatee_two,
+ 100
+ ));
+
+ // delegatee one tries to delegate to delegatee 2
+ assert_noop!(
+ DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegatee_one).into(),
+ delegatee_two,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // delegatee one tries to delegate to a delegator
+ assert_noop!(
+ DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegatee_one).into(),
+ delegator_one,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+ assert_noop!(
+ DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegatee_one).into(),
+ delegator_two,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+
+ // delegator one tries to delegate to delegatee 2 as well (it already delegates to delegatee
+ // 1)
+ assert_noop!(
+ DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegator_one).into(),
+ delegatee_two,
+ 10
+ ),
+ Error::::InvalidDelegation
+ );
+ });
+}
+
+#[test]
+fn apply_pending_slash() {
+ ExtBuilder::default().build_and_execute(|| {
+ (
+ // fixme(ank4n): add tests for apply_pending_slash
+ )
+ });
+}
+
+/// 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 delegatee: AccountId = 99;
+ let reward_acc: AccountId = 100;
+ assert_eq!(Staking::status(&delegatee), Err(StakingError::::NotStash.into()));
+
+ // set intention to become a delegatee
+ fund(&delegatee, 100);
+ assert_ok!(DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(delegatee).into(),
+ reward_acc
+ ));
+ assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 0);
+
+ let mut delegated_balance: Balance = 0;
+
+ // set some delegations
+ for delegator in 200..250 {
+ fund(&delegator, 200);
+ assert_ok!(DelegatedStaking::delegate_funds(
+ RawOrigin::Signed(delegator).into(),
+ delegatee,
+ 100
+ ));
+ delegated_balance += 100;
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator),
+ 100
+ );
+
+ let delegatee_obj = get_delegatee(&delegatee);
+ assert_eq!(delegatee_obj.ledger.stakeable_balance(), delegated_balance);
+ assert_eq!(delegatee_obj.available_to_bond(), 0);
+ assert_eq!(delegatee_obj.bonded_stake(), delegated_balance);
+ }
+
+ assert_eq!(
+ Staking::stake(&delegatee).unwrap(),
+ Stake { total: 50 * 100, active: 50 * 100 }
+ )
+ });
+ }
+
+ #[test]
+ fn withdraw_test() {
+ ExtBuilder::default().build_and_execute(|| {
+ // initial era
+ start_era(1);
+ let delegatee: AccountId = 200;
+ let reward_acc: AccountId = 201;
+ let delegators: Vec = (301..=350).collect();
+ let total_staked =
+ setup_delegation_stake(delegatee, reward_acc, delegators.clone(), 10, 10);
+
+ // lets go to a new era
+ start_era(2);
+
+ assert!(eq_stake(delegatee, total_staked, total_staked));
+ // Withdrawing without unbonding would fail.
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 301, 50, 0),
+ Error::::NotEnoughFunds
+ );
+ // assert_noop!(DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 200, 50,
+ // 0), Error::::NotAllowed); active and total stake remains same
+ assert!(eq_stake(delegatee, total_staked, total_staked));
+
+ // 305 wants to unbond 50 in era 2, withdrawable in era 5.
+ assert_ok!(DelegatedStaking::unbond(&delegatee, 50));
+ // 310 wants to unbond 100 in era 3, withdrawable in era 6.
+ start_era(3);
+ assert_ok!(DelegatedStaking::unbond(&delegatee, 100));
+ // 320 wants to unbond 200 in era 4, withdrawable in era 7.
+ start_era(4);
+ assert_ok!(DelegatedStaking::unbond(&delegatee, 200));
+
+ // active stake is now reduced..
+ let expected_active = total_staked - (50 + 100 + 200);
+ assert!(eq_stake(delegatee, total_staked, expected_active));
+
+ // nothing to withdraw at era 4
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 305, 50, 0),
+ Error::::NotEnoughFunds
+ );
+
+ assert!(eq_stake(delegatee, total_staked, expected_active));
+ assert_eq!(get_delegatee(&delegatee).available_to_bond(), 0);
+ // full amount is still delegated
+ assert_eq!(get_delegatee(&delegatee).ledger.effective_balance(), total_staked);
+
+ start_era(5);
+ // at era 5, 50 tokens are withdrawable, cannot withdraw more.
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 305, 51, 0),
+ Error::::NotEnoughFunds
+ );
+ // less is possible
+ assert_ok!(DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 305, 30, 0));
+ assert_ok!(DelegatedStaking::release(RawOrigin::Signed(delegatee).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(RawOrigin::Signed(delegatee).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(RawOrigin::Signed(delegatee).into(), 309, 91, 0),
+ Error::::NotEnoughFunds
+ );
+ // 310 cannot withdraw more than delegated funds.
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 310, 101, 0),
+ Error::::NotEnoughFunds
+ );
+ // but can withdraw all its delegation amount.
+ assert_ok!(DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 310, 100, 0));
+ // 320 can withdraw all its delegation amount.
+ assert_ok!(DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 320, 200, 0));
+
+ // cannot withdraw anything more..
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 301, 1, 0),
+ Error::::NotEnoughFunds
+ );
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 350, 1, 0),
+ Error::::NotEnoughFunds
+ );
+ });
+ }
+
+ #[test]
+ fn withdraw_happens_with_unbonded_balance_first() {
+ ExtBuilder::default().build_and_execute(|| {
+ let delegatee = 200;
+ setup_delegation_stake(delegatee, 201, (300..350).collect(), 100, 0);
+
+ // verify withdraw not possible yet
+ assert_noop!(
+ DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 300, 100, 0),
+ Error::::NotEnoughFunds
+ );
+
+ // add new delegation that is not staked
+
+ // FIXME(ank4n): add scenario where staked funds are withdrawn from ledger but not
+ // withdrawn and test its claimed from there first.
+
+ // fund(&300, 1000);
+ // assert_ok!(DelegatedStaking::delegate_funds(RawOrigin::Signed(300.into()), delegate,
+ // 100));
+ //
+ // // verify unbonded balance
+ // assert_eq!(get_delegatee(&delegatee).available_to_bond(), 100);
+ //
+ // // withdraw works now without unbonding
+ // assert_ok!(DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 300, 100,
+ // 0)); assert_eq!(get_delegatee(&delegatee).available_to_bond(), 0);
+ });
+ }
+
+ #[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);
+
+ // `delegatee` account cannot be reward destination
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(RawOrigin::Signed(200).into(), 200),
+ Error::::InvalidRewardDestination
+ );
+
+ // different reward account works
+ assert_ok!(DelegatedStaking::register_as_delegatee(RawOrigin::Signed(200).into(), 201));
+ // add some delegations to it
+ fund(&300, 1000);
+ assert_ok!(DelegatedStaking::delegate_funds(RawOrigin::Signed(300).into(), 200, 100));
+
+ // if delegate calls Staking pallet directly with a different reward destination, it
+ // fails.
+ assert_noop!(
+ Staking::set_payee(RuntimeOrigin::signed(200), RewardDestination::Stash),
+ StakingError::::RewardDestinationRestricted
+ );
+
+ // passing correct reward destination works
+ assert_ok!(Staking::set_payee(
+ RuntimeOrigin::signed(200),
+ RewardDestination::Account(201)
+ ));
+
+ // amount is staked correctly
+ assert!(eq_stake(200, 100, 100));
+ assert_eq!(get_delegatee(&200).available_to_bond(), 0);
+ assert_eq!(get_delegatee(&200).ledger.effective_balance(), 100);
+
+ // free balance of delegate is untouched
+ assert_eq!(Balances::free_balance(200), balance_200);
+ });
+ }
+
+ #[test]
+ fn delegatee_restrictions() {
+ ExtBuilder::default().build_and_execute(|| {
+ setup_delegation_stake(200, 201, (202..203).collect(), 100, 0);
+
+ // Registering again is noop
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(RawOrigin::Signed(200).into(), 201),
+ Error::::NotAllowed
+ );
+ // a delegator cannot become delegate
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(RawOrigin::Signed(202).into(), 203),
+ Error::::NotAllowed
+ );
+ // existing staker cannot become a delegate
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(GENESIS_NOMINATOR_ONE).into(),
+ 201
+ ),
+ Error::::AlreadyStaking
+ );
+ assert_noop!(
+ DelegatedStaking::register_as_delegatee(
+ RawOrigin::Signed(GENESIS_VALIDATOR).into(),
+ 201
+ ),
+ Error::::AlreadyStaking
+ );
+ });
+ }
+
+ #[test]
+ fn slash_works() {
+ ExtBuilder::default().build_and_execute(|| {
+ setup_delegation_stake(200, 201, (210..250).collect(), 100, 0);
+ start_era(1);
+ // fixme(ank4n): add tests for slashing
+ });
+ }
+
+ #[test]
+ fn migration_works() {
+ ExtBuilder::default().build_and_execute(|| {
+ // add a nominator
+ fund(&200, 5000);
+ let staked_amount = 4000;
+ 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), 5000);
+
+ // 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_delegatee(RawOrigin::Signed(200).into(), 201));
+
+ // verify all went well
+ let mut expected_proxy_delegated_amount = staked_amount;
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::Delegating.into(), &proxy_delegator),
+ expected_proxy_delegated_amount
+ );
+ // ED + stake amount is transferred from delegate to proxy delegator account.
+ assert_eq!(
+ Balances::free_balance(200),
+ 5000 - staked_amount - ExistentialDeposit::get()
+ );
+ assert_eq!(DelegatedStaking::stake(&200).unwrap(), init_stake);
+ assert_eq!(get_delegatee(&200).ledger.effective_balance(), 4000);
+ assert_eq!(get_delegatee(&200).available_to_bond(), 0);
+
+ // now lets migrate the delegators
+ let delegator_share = staked_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::Delegating.into(), &delegator),
+ delegator_share
+ );
+ expected_proxy_delegated_amount -= delegator_share;
+ assert_eq!(
+ Balances::balance_on_hold(&HoldReason::Delegating.into(), &proxy_delegator),
+ expected_proxy_delegated_amount
+ );
+
+ // delegate stake is unchanged.
+ assert_eq!(DelegatedStaking::stake(&200).unwrap(), init_stake);
+ assert_eq!(get_delegatee(&200).ledger.effective_balance(), 4000);
+ assert_eq!(get_delegatee(&200).available_to_bond(), 0);
+ }
+
+ // cannot use migrate delegator anymore
+ assert_noop!(
+ DelegatedStaking::migrate_delegation(RawOrigin::Signed(200).into(), 305, 1),
+ Error::::NotEnoughFunds
+ );
+ });
+ }
+}
+
+// FIMXE(ank4n): Move these integration test to nomination pools.
+mod pool_integration {
+ use super::*;
+ use pallet_nomination_pools::{BondExtra, BondedPools, PoolState};
+
+ #[test]
+ fn create_pool_test() {
+ ExtBuilder::default().build_and_execute(|| {
+ let creator: AccountId = 100;
+ fund(&creator, 500);
+ let delegate_amount = 200;
+
+ // nothing held initially
+ assert_eq!(held_balance(&creator), 0);
+
+ // create pool
+ assert_ok!(Pools::create(
+ RawOrigin::Signed(creator).into(),
+ delegate_amount,
+ creator,
+ creator,
+ creator
+ ));
+
+ // correct amount is locked in depositor's account.
+ assert_eq!(held_balance(&creator), delegate_amount);
+
+ let pool_account = Pools::create_bonded_account(1);
+ let delegatee = get_delegatee(&pool_account);
+
+ // verify state
+ assert_eq!(delegatee.ledger.effective_balance(), delegate_amount);
+ assert_eq!(delegatee.available_to_bond(), 0);
+ assert_eq!(delegatee.total_unbonded(), 0);
+ });
+ }
+
+ #[test]
+ fn join_pool() {
+ ExtBuilder::default().build_and_execute(|| {
+ // create a pool
+ let pool_id = create_pool(100, 200);
+ // keep track of staked amount.
+ let mut staked_amount: Balance = 200;
+
+ // fund delegator
+ let delegator: AccountId = 300;
+ fund(&delegator, 500);
+ // nothing held initially
+ assert_eq!(held_balance(&delegator), 0);
+
+ // delegator joins pool
+ assert_ok!(Pools::join(RawOrigin::Signed(delegator).into(), 100, pool_id));
+ staked_amount += 100;
+
+ // correct amount is locked in depositor's account.
+ assert_eq!(held_balance(&delegator), 100);
+
+ // delegator is not actively exposed to core staking.
+ assert_eq!(Staking::status(&delegator), Err(StakingError::::NotStash.into()));
+
+ let pool_delegatee = get_delegatee(&Pools::create_bonded_account(1));
+ // verify state
+ assert_eq!(pool_delegatee.ledger.effective_balance(), staked_amount);
+ assert_eq!(pool_delegatee.bonded_stake(), staked_amount);
+ assert_eq!(pool_delegatee.available_to_bond(), 0);
+ assert_eq!(pool_delegatee.total_unbonded(), 0);
+
+ // let a bunch of delegators join this pool
+ for i in 301..350 {
+ fund(&i, 500);
+ assert_ok!(Pools::join(RawOrigin::Signed(i).into(), (100 + i).into(), pool_id));
+ staked_amount += 100 + i;
+ assert_eq!(held_balance(&i), 100 + i);
+ }
+
+ let pool_delegatee = pool_delegatee.refresh().unwrap();
+ assert_eq!(pool_delegatee.ledger.effective_balance(), staked_amount);
+ assert_eq!(pool_delegatee.bonded_stake(), staked_amount);
+ assert_eq!(pool_delegatee.available_to_bond(), 0);
+ assert_eq!(pool_delegatee.total_unbonded(), 0);
+ });
+ }
+
+ #[test]
+ fn bond_extra_to_pool() {
+ ExtBuilder::default().build_and_execute(|| {
+ let pool_id = create_pool(100, 200);
+ add_delegators_to_pool(pool_id, (300..310).collect(), 100);
+ let mut staked_amount = 200 + 100 * 10;
+ assert_eq!(get_pool_delegatee(pool_id).bonded_stake(), staked_amount);
+
+ // bond extra to pool
+ for i in 300..310 {
+ assert_ok!(Pools::bond_extra(
+ RawOrigin::Signed(i).into(),
+ BondExtra::FreeBalance(50)
+ ));
+ staked_amount += 50;
+ assert_eq!(get_pool_delegatee(pool_id).bonded_stake(), staked_amount);
+ }
+ });
+ }
+
+ #[test]
+ fn claim_pool_rewards() {
+ ExtBuilder::default().build_and_execute(|| {
+ let creator = 100;
+ let creator_stake = 1000;
+ let pool_id = create_pool(creator, creator_stake);
+ add_delegators_to_pool(pool_id, (300..310).collect(), 100);
+ add_delegators_to_pool(pool_id, (310..320).collect(), 200);
+ let total_staked = creator_stake + 100 * 10 + 200 * 10;
+
+ // give some rewards
+ let reward_acc = Pools::create_reward_account(pool_id);
+ let reward_amount = 1000;
+ fund(&reward_acc, reward_amount);
+
+ // claim rewards
+ for i in 300..320 {
+ let pre_balance = Balances::free_balance(i);
+ let delegator_staked_balance = held_balance(&i);
+ // payout reward
+ assert_ok!(Pools::claim_payout(RawOrigin::Signed(i).into()));
+
+ let reward = Balances::free_balance(i) - pre_balance;
+ assert_eq!(reward, delegator_staked_balance * reward_amount / total_staked);
+ }
+
+ // payout creator
+ let pre_balance = Balances::free_balance(creator);
+ assert_ok!(Pools::claim_payout(RawOrigin::Signed(creator).into()));
+ // verify they are paid out correctly
+ let reward = Balances::free_balance(creator) - pre_balance;
+ assert_eq!(reward, creator_stake * reward_amount / total_staked);
+
+ // reward account should only have left minimum balance after paying out everyone.
+ assert_eq!(Balances::free_balance(reward_acc), ExistentialDeposit::get());
+ });
+ }
+
+ #[test]
+ fn withdraw_from_pool() {
+ ExtBuilder::default().build_and_execute(|| {
+ // initial era
+ start_era(1);
+
+ let pool_id = create_pool(100, 1000);
+ let bond_amount = 200;
+ add_delegators_to_pool(pool_id, (300..310).collect(), bond_amount);
+ let total_staked = 1000 + bond_amount * 10;
+ let pool_acc = Pools::create_bonded_account(pool_id);
+
+ start_era(2);
+ // nothing to release yet.
+ assert_noop!(
+ Pools::withdraw_unbonded(RawOrigin::Signed(301).into(), 301, 0),
+ PoolsError::::SubPoolsNotFound
+ );
+
+ // 301 wants to unbond 50 in era 2, withdrawable in era 5.
+ assert_ok!(Pools::unbond(RawOrigin::Signed(301).into(), 301, 50));
+
+ // 302 wants to unbond 100 in era 3, withdrawable in era 6.
+ start_era(3);
+ assert_ok!(Pools::unbond(RawOrigin::Signed(302).into(), 302, 100));
+
+ // 303 wants to unbond 200 in era 4, withdrawable in era 7.
+ start_era(4);
+ assert_ok!(Pools::unbond(RawOrigin::Signed(303).into(), 303, 200));
+
+ // active stake is now reduced..
+ let expected_active = total_staked - (50 + 100 + 200);
+ assert!(eq_stake(pool_acc, total_staked, expected_active));
+
+ // nothing to withdraw at era 4
+ for i in 301..310 {
+ assert_noop!(
+ Pools::withdraw_unbonded(RawOrigin::Signed(i).into(), i, 0),
+ PoolsError::::CannotWithdrawAny
+ );
+ }
+
+ assert!(eq_stake(pool_acc, total_staked, expected_active));
+
+ start_era(5);
+ // at era 5, 301 can withdraw.
+
+ System::reset_events();
+ let held_301 = held_balance(&301);
+ let free_301 = Balances::free_balance(301);
+
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(301).into(), 301, 0));
+ assert_eq!(
+ events_since_last_call(),
+ vec![Event::Released { delegatee: pool_acc, delegator: 301, amount: 50 }]
+ );
+ assert_eq!(
+ pool_events_since_last_call(),
+ vec![PoolsEvent::Withdrawn { member: 301, pool_id, balance: 50, points: 50 }]
+ );
+ assert_eq!(held_balance(&301), held_301 - 50);
+ assert_eq!(Balances::free_balance(301), free_301 + 50);
+
+ start_era(7);
+ // era 7 both delegators can withdraw
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(302).into(), 302, 0));
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(303).into(), 303, 0));
+
+ assert_eq!(
+ events_since_last_call(),
+ vec![
+ Event::Released { delegatee: pool_acc, delegator: 302, amount: 100 },
+ Event::Released { delegatee: pool_acc, delegator: 303, amount: 200 },
+ ]
+ );
+ assert_eq!(
+ pool_events_since_last_call(),
+ vec![
+ PoolsEvent::Withdrawn { member: 302, pool_id, balance: 100, points: 100 },
+ PoolsEvent::Withdrawn { member: 303, pool_id, balance: 200, points: 200 },
+ PoolsEvent::MemberRemoved { pool_id: 1, member: 303 },
+ ]
+ );
+
+ // 303 is killed
+ assert!(!Delegators::::contains_key(303));
+ });
+ }
+
+ #[test]
+ fn pool_withdraw_unbonded() {
+ ExtBuilder::default().build_and_execute(|| {
+ // initial era
+ start_era(1);
+ let pool_id = create_pool(100, 1000);
+ add_delegators_to_pool(pool_id, (300..310).collect(), 200);
+
+ start_era(2);
+ // 1000 tokens to be unbonded in era 5.
+ for i in 300..310 {
+ assert_ok!(Pools::unbond(RawOrigin::Signed(i).into(), i, 100));
+ }
+
+ start_era(3);
+ // 500 tokens to be unbonded in era 6.
+ for i in 300..310 {
+ assert_ok!(Pools::unbond(RawOrigin::Signed(i).into(), i, 50));
+ }
+
+ start_era(5);
+ // withdraw pool should withdraw 1000 tokens
+ assert_ok!(Pools::pool_withdraw_unbonded(RawOrigin::Signed(100).into(), pool_id, 0));
+ assert_eq!(get_pool_delegatee(pool_id).total_unbonded(), 1000);
+
+ start_era(6);
+ // should withdraw 500 more
+ assert_ok!(Pools::pool_withdraw_unbonded(RawOrigin::Signed(100).into(), pool_id, 0));
+ assert_eq!(get_pool_delegatee(pool_id).total_unbonded(), 1000 + 500);
+
+ start_era(7);
+ // Nothing to withdraw, still at 1500.
+ assert_ok!(Pools::pool_withdraw_unbonded(RawOrigin::Signed(100).into(), pool_id, 0));
+ assert_eq!(get_pool_delegatee(pool_id).total_unbonded(), 1500);
+ });
+ }
+
+ #[test]
+ fn update_nominations() {
+ ExtBuilder::default().build_and_execute(|| {
+ start_era(1);
+ // can't nominate for non-existent pool
+ assert_noop!(
+ Pools::nominate(RawOrigin::Signed(100).into(), 1, vec![99]),
+ PoolsError::::PoolNotFound
+ );
+
+ let pool_id = create_pool(100, 1000);
+ let pool_acc = Pools::create_bonded_account(pool_id);
+ assert_ok!(Pools::nominate(RawOrigin::Signed(100).into(), 1, vec![20, 21, 22]));
+ assert!(Staking::status(&pool_acc) == Ok(StakerStatus::Nominator(vec![20, 21, 22])));
+
+ start_era(3);
+ assert_ok!(Pools::nominate(RawOrigin::Signed(100).into(), 1, vec![18, 19, 22]));
+ assert!(Staking::status(&pool_acc) == Ok(StakerStatus::Nominator(vec![18, 19, 22])));
+ });
+ }
+
+ #[test]
+ fn destroy_pool() {
+ ExtBuilder::default().build_and_execute(|| {
+ start_era(1);
+ let creator = 100;
+ let creator_stake = 1000;
+ let pool_id = create_pool(creator, creator_stake);
+ add_delegators_to_pool(pool_id, (300..310).collect(), 200);
+
+ start_era(3);
+ // lets destroy the pool
+ assert_ok!(Pools::set_state(
+ RawOrigin::Signed(creator).into(),
+ pool_id,
+ PoolState::Destroying
+ ));
+ assert_ok!(Pools::chill(RawOrigin::Signed(creator).into(), pool_id));
+
+ // unbond all members by the creator/admin
+ for i in 300..310 {
+ assert_ok!(Pools::unbond(RawOrigin::Signed(creator).into(), i, 200));
+ }
+
+ start_era(6);
+ // withdraw all members by the creator/admin
+ for i in 300..310 {
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(creator).into(), i, 0));
+ }
+
+ // unbond creator
+ assert_ok!(Pools::unbond(RawOrigin::Signed(creator).into(), creator, creator_stake));
+
+ start_era(9);
+ System::reset_events();
+ // Withdraw self
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(creator).into(), creator, 0));
+ assert_eq!(
+ pool_events_since_last_call(),
+ vec![
+ PoolsEvent::Withdrawn {
+ member: creator,
+ pool_id,
+ balance: creator_stake,
+ points: creator_stake,
+ },
+ PoolsEvent::MemberRemoved { pool_id, member: creator },
+ PoolsEvent::Destroyed { pool_id },
+ ]
+ );
+
+ // Make sure all data is cleaned up.
+ assert_eq!(Delegatees::::contains_key(Pools::create_bonded_account(pool_id)), false);
+ assert_eq!(Delegators::::contains_key(creator), false);
+ for i in 300..310 {
+ assert_eq!(Delegators::::contains_key(i), false);
+ }
+ });
+ }
+
+ #[test]
+ fn pool_slashed() {
+ ExtBuilder::default().build_and_execute(|| {
+ start_era(1);
+ let creator = 100;
+ let creator_stake = 500;
+ let pool_id = create_pool(creator, creator_stake);
+ let delegator_stake = 100;
+ add_delegators_to_pool(pool_id, (300..306).collect(), delegator_stake);
+ let pool_acc = Pools::create_bonded_account(pool_id);
+
+ let total_staked = creator_stake + delegator_stake * 6;
+ assert_eq!(Staking::stake(&pool_acc).unwrap().total, total_staked);
+
+ // lets unbond a delegator each in next eras (2, 3, 4).
+ start_era(2);
+ assert_ok!(Pools::unbond(RawOrigin::Signed(300).into(), 300, delegator_stake));
+
+ start_era(3);
+ assert_ok!(Pools::unbond(RawOrigin::Signed(301).into(), 301, delegator_stake));
+
+ start_era(4);
+ assert_ok!(Pools::unbond(RawOrigin::Signed(302).into(), 302, delegator_stake));
+ System::reset_events();
+
+ // slash the pool at era 3
+ assert_eq!(
+ BondedPools::::get(1).unwrap().points,
+ creator_stake + delegator_stake * 6 - delegator_stake * 3
+ );
+ pallet_staking::slashing::do_slash::(
+ &pool_acc,
+ 500,
+ &mut Default::default(),
+ &mut Default::default(),
+ 3,
+ );
+
+ assert_eq!(
+ pool_events_since_last_call(),
+ vec![
+ // 300 did not get slashed as all as it unbonded in an era before slash.
+ // 301 got slashed 50% of 100 = 50.
+ PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 6, balance: 50 },
+ // 302 got slashed 50% of 100 = 50.
+ PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 7, balance: 50 },
+ // Rest of the pool slashed 50% of 800 = 400.
+ PoolsEvent::PoolSlashed { pool_id: 1, balance: 400 },
+ ]
+ );
+
+ // slash is lazy and balance is still locked in user's accounts.
+ assert_eq!(held_balance(&creator), creator_stake);
+ for i in 300..306 {
+ assert_eq!(held_balance(&i), delegator_stake);
+ }
+ assert_eq!(
+ get_pool_delegatee(pool_id).ledger.effective_balance(),
+ Staking::total_stake(&pool_acc).unwrap()
+ );
+
+ // pending slash is book kept.
+ assert_eq!(get_pool_delegatee(pool_id).ledger.pending_slash, 500);
+
+ // go in some distant future era.
+ start_era(10);
+ System::reset_events();
+
+ // 300 is not slashed and can withdraw all balance.
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(300).into(), 300, 1));
+ assert_eq!(
+ events_since_last_call(),
+ vec![Event::Released { delegatee: pool_acc, delegator: 300, amount: 100 }]
+ );
+ assert_eq!(get_pool_delegatee(pool_id).ledger.pending_slash, 500);
+
+ // withdraw the other two delegators (301 and 302) who were unbonding.
+ for i in 301..=302 {
+ let pre_balance = Balances::free_balance(i);
+ let pre_pending_slash = get_pool_delegatee(pool_id).ledger.pending_slash;
+ assert_ok!(Pools::withdraw_unbonded(RawOrigin::Signed(i).into(), i, 0));
+ assert_eq!(
+ events_since_last_call(),
+ vec![
+ Event::Slashed { delegatee: pool_acc, delegator: i, amount: 50 },
+ Event::Released { delegatee: pool_acc, delegator: i, amount: 50 },
+ ]
+ );
+ assert_eq!(
+ get_pool_delegatee(pool_id).ledger.pending_slash,
+ pre_pending_slash - 50
+ );
+ assert_eq!(held_balance(&i), 0);
+ assert_eq!(Balances::free_balance(i) - pre_balance, 50);
+ }
+
+ // let's update all the slash
+ let slash_reporter = 99;
+ // give our reporter some balance.
+ fund(&slash_reporter, 100);
+
+ for i in 303..306 {
+ let pre_pending_slash = get_pool_delegatee(pool_id).ledger.pending_slash;
+ assert_ok!(Pools::apply_slash(RawOrigin::Signed(slash_reporter).into(), i));
+
+ // each member is slashed 50% of 100 = 50.
+ assert_eq!(
+ get_pool_delegatee(pool_id).ledger.pending_slash,
+ pre_pending_slash - 50
+ );
+ // left with 50.
+ assert_eq!(held_balance(&i), 50);
+ }
+ // reporter is paid SlashRewardFraction of the slash, i.e. 10% of 50 = 5
+ assert_eq!(Balances::free_balance(slash_reporter), 100 + 5 * 3);
+ // slash creator
+ assert_ok!(Pools::apply_slash(RawOrigin::Signed(slash_reporter).into(), creator));
+ // all slash should be applied now.
+ assert_eq!(get_pool_delegatee(pool_id).ledger.pending_slash, 0);
+ // for creator, 50% of stake should be slashed (250), 10% of which should go to reporter
+ // (25).
+ assert_eq!(Balances::free_balance(slash_reporter), 115 + 25);
+ });
+ }
+
+ fn create_pool(creator: AccountId, amount: Balance) -> u32 {
+ fund(&creator, amount * 2);
+ assert_ok!(Pools::create(
+ RawOrigin::Signed(creator).into(),
+ amount,
+ creator,
+ creator,
+ creator
+ ));
+
+ pallet_nomination_pools::LastPoolId::::get()
+ }
+
+ fn add_delegators_to_pool(pool_id: u32, delegators: Vec, amount: Balance) {
+ for delegator in delegators {
+ fund(&delegator, amount * 2);
+ assert_ok!(Pools::join(RawOrigin::Signed(delegator).into(), amount, pool_id));
+ }
+ }
+
+ fn get_pool_delegatee(pool_id: u32) -> Delegatee {
+ get_delegatee(&Pools::create_bonded_account(pool_id))
+ }
+}
diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs
new file mode 100644
index 000000000000..0dcdcc0659a4
--- /dev/null
+++ b/substrate/frame/delegated-staking/src/types.rs
@@ -0,0 +1,314 @@
+// 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 a `delegatee` account.
+ ///
+ /// Funds for unmigrated `delegator` accounts of the `delegatee` 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 delegatee: 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 from(delegatee: &T::AccountId, amount: BalanceOf) -> Self {
+ Delegation { delegatee: delegatee.clone(), amount }
+ }
+
+ /// Ensure the delegator is either a new delegator or they are adding more delegation to the
+ /// existing delegatee.
+ ///
+ /// Delegators are prevented from delegating to multiple delegatees at the same time.
+ pub(crate) fn can_delegate(delegator: &T::AccountId, delegatee: &T::AccountId) -> bool {
+ Delegation::::get(delegator)
+ .map(|delegation| delegation.delegatee == delegatee.clone())
+ .unwrap_or(
+ // all good if its a new delegator except it should not be an existing delegatee.
+ !>::contains_key(delegator),
+ )
+ }
+
+ /// Checked decrease of delegation amount. Consumes self and returns a new copy.
+ pub(crate) fn decrease_delegation(self, amount: BalanceOf) -> Option {
+ let updated_delegation = self.amount.checked_sub(&amount)?;
+ Some(Delegation::from(&self.delegatee, updated_delegation))
+ }
+
+ /// Checked increase of delegation amount. Consumes self and returns a new copy.
+ #[allow(unused)]
+ pub(crate) fn increase_delegation(self, amount: BalanceOf) -> Option {
+ let updated_delegation = self.amount.checked_add(&amount)?;
+ Some(Delegation::from(&self.delegatee, updated_delegation))
+ }
+
+ /// Save self to storage. If the delegation amount is zero, remove the delegation.
+ pub(crate) fn save_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 a `Delegatee`.
+///
+/// This keeps track of the active balance of the `delegatee` that is made up from the funds that
+/// are currently delegated to this `delegatee`. 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 DelegateeLedger {
+ /// Where the reward should be paid out.
+ pub payee: T::AccountId,
+ /// Sum of all delegated funds to this `delegatee`.
+ #[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 `delegatee` does not bond funds from delegated
+ /// funds that are withdrawn and should be claimed by delegators.
+ // FIXME(ank4n): Check/test about rebond: where delegator rebond what is unlocking.
+ #[codec(compact)]
+ pub unclaimed_withdrawals: BalanceOf,
+ /// Slashes that are not yet applied. This affects the effective balance of the `delegatee`.
+ #[codec(compact)]
+ pub pending_slash: BalanceOf