From dda12d499db3da8a4acd3bc84472a77a7d2a3f2a Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 29 Mar 2024 10:13:36 +0100 Subject: [PATCH 01/85] add ability to super bond to staking --- substrate/frame/staking/src/ledger.rs | 32 ++++-- substrate/frame/staking/src/pallet/impls.rs | 119 +++++++++++++++++++- substrate/frame/staking/src/pallet/mod.rs | 44 +++----- substrate/frame/staking/src/slashing.rs | 22 ++-- substrate/primitives/staking/src/lib.rs | 95 +++++++++++++++- 5 files changed, 259 insertions(+), 53 deletions(-) diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs index 9461daefed65..641d7c81ae41 100644 --- a/substrate/frame/staking/src/ledger.rs +++ b/substrate/frame/staking/src/ledger.rs @@ -33,13 +33,14 @@ use frame_support::{ defensive, ensure, - traits::{Defensive, LockableCurrency, WithdrawReasons}, + traits::{Defensive, LockableCurrency}, }; use sp_staking::StakingAccount; use sp_std::prelude::*; use crate::{ - BalanceOf, Bonded, Config, Error, Ledger, Payee, RewardDestination, StakingLedger, STAKING_ID, + BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger, + STAKING_ID, }; #[cfg(any(feature = "runtime-benchmarks", test))] @@ -187,7 +188,7 @@ impl StakingLedger { return Err(Error::::NotStash) } - T::Currency::set_lock(STAKING_ID, &self.stash, self.total, WithdrawReasons::all()); + Pallet::::update_lock(&self.stash, self.total).map_err(|_| Error::::BadState)?; Ledger::::insert( &self.controller().ok_or_else(|| { defensive!("update called on a ledger that is not bonded."); @@ -204,22 +205,29 @@ impl StakingLedger { /// It sets the reward preferences for the bonded stash. pub(crate) fn bond(self, payee: RewardDestination) -> Result<(), Error> { if >::contains_key(&self.stash) { - Err(Error::::AlreadyBonded) - } else { - >::insert(&self.stash, payee); - >::insert(&self.stash, &self.stash); - self.update() + return Err(Error::::AlreadyBonded) + } + if Pallet::::restrict_reward_destination(&self.stash, payee.clone()) { + return Err(Error::::RewardDestinationRestricted); } + + >::insert(&self.stash, payee); + >::insert(&self.stash, &self.stash); + self.update() } /// Sets the ledger Payee. pub(crate) fn set_payee(self, payee: RewardDestination) -> Result<(), Error> { if !>::contains_key(&self.stash) { - Err(Error::::NotStash) - } else { - >::insert(&self.stash, payee); - Ok(()) + return Err(Error::::NotStash) + } + + if Pallet::::restrict_reward_destination(&self.stash, payee.clone()) { + return Err(Error::::RewardDestinationRestricted); } + + >::insert(&self.stash, payee); + Ok(()) } /// Sets the ledger controller to its stash. diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 2f43e4847e45..a7428d9ad905 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -28,14 +28,16 @@ use frame_support::{ pallet_prelude::*, traits::{ Currency, Defensive, DefensiveSaturating, EstimateNextNewSession, Get, Imbalance, - InspectLockableCurrency, Len, OnUnbalanced, TryCollect, UnixTime, + InspectLockableCurrency, Len, LockableCurrency, OnUnbalanced, TryCollect, UnixTime, }, weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; use pallet_session::historical; use sp_runtime::{ - traits::{Bounded, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero}, + traits::{ + Bounded, CheckedSub, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero, + }, Perbill, Percent, }; use sp_staking::{ @@ -149,6 +151,37 @@ impl Pallet { Self::slashable_balance_of_vote_weight(who, issuance) } + pub(super) fn do_bond_extra(stash: &T::AccountId, additional: BalanceOf) -> DispatchResult { + let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?; + + let extra = if Self::is_virtual_nominator(stash) { + additional + } else { + // additional amount or actual balance of stash whichever is lower. + additional.min( + T::Currency::free_balance(stash) + .checked_sub(&ledger.total) + .ok_or(sp_runtime::ArithmeticError::Overflow)?, + ) + }; + + ledger.total += extra; + ledger.active += extra; + // Last check: the new active amount of ledger must be more than ED. + ensure!(ledger.active >= T::Currency::minimum_balance(), Error::::InsufficientBond); + + // NOTE: ledger must be updated prior to calling `Self::weight_of`. + ledger.update()?; + // update this staker in the sorted list, if they exist in it. + if T::VoterList::contains(stash) { + let _ = T::VoterList::on_update(&stash, Self::weight_of(stash)).defensive(); + } + + Self::deposit_event(Event::::Bonded { stash: stash.clone(), amount: extra }); + + Ok(()) + } + pub(super) fn do_withdraw_unbonded( controller: &T::AccountId, num_slashing_spans: u32, @@ -1132,6 +1165,45 @@ impl Pallet { ) -> Exposure> { EraInfo::::get_full_exposure(era, account) } + + /// Whether the passed reward destination is restricted for the given account. + /// + /// Virtual nominators are not allowed to compound their rewards as this pallet does not manage + /// locks for them. For external pallets that manage the virtual bond, it is their + /// responsibility to distribute the reward and re-bond them. + /// + /// Conservatively, we expect them to always set the reward destination to a non stash account. + pub(crate) fn restrict_reward_destination( + who: &T::AccountId, + reward_destination: RewardDestination, + ) -> bool { + Self::is_virtual_nominator(who) && + match reward_destination { + RewardDestination::Account(payee) => payee == *who, + _ => true, + } + } + + pub(crate) fn is_virtual_nominator(who: &T::AccountId) -> bool { + VirtualNominators::::contains_key(who) + } + + pub(crate) fn update_lock( + who: &T::AccountId, + amount: BalanceOf, + ) -> sp_runtime::DispatchResult { + // Skip locking virtual nominators. They are handled by external pallets. + if !Self::is_virtual_nominator(who) { + T::Currency::set_lock( + crate::STAKING_ID, + who, + amount, + frame_support::traits::WithdrawReasons::all(), + ); + } + + Ok(()) + } } impl Pallet { @@ -1748,6 +1820,15 @@ impl StakingInterface for Pallet { .map(|_| ()) } + fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult { + // since controller is deprecated and this function is never used for old ledgers with + // distinct controllers, we can safely assume that stash is the controller. + Self::set_payee( + RawOrigin::Signed(stash.clone()).into(), + RewardDestination::Account(reward_acc.clone()), + ) + } + fn chill(who: &Self::AccountId) -> DispatchResult { // defensive-only: any account bonded via this interface has the stash set as the // controller, but we have to be sure. Same comment anywhere else that we read this. @@ -1832,6 +1913,10 @@ impl StakingInterface for Pallet { } } + fn slash_reward_fraction() -> Perbill { + SlashRewardFraction::::get() + } + sp_staking::runtime_benchmarks_enabled! { fn nominations(who: &Self::AccountId) -> Option> { Nominators::::get(who).map(|n| n.targets.into_inner()) @@ -1860,6 +1945,36 @@ impl StakingInterface for Pallet { } } +impl sp_staking::StakingUnsafe for Pallet { + fn force_release(who: &Self::AccountId) { + T::Currency::remove_lock(crate::STAKING_ID, who) + } + + fn virtual_bond( + who: &Self::AccountId, + value: Self::Balance, + payee: &Self::AccountId, + ) -> DispatchResult { + if StakingLedger::::is_bonded(StakingAccount::Stash(who.clone())) { + return Err(Error::::AlreadyBonded.into()) + } + + frame_system::Pallet::::inc_consumers(&who).map_err(|_| Error::::BadState)?; + + // mark who as a virtual nominator + VirtualNominators::::insert(who, ()); + + Self::deposit_event(Event::::Bonded { stash: who.clone(), amount: value }); + let ledger = StakingLedger::::new(who.clone(), value); + + // You're auto-bonded forever, here. We might improve this by only bonding when + // you actually validate/nominate and remove once you unbond __everything__. + ledger.bond(RewardDestination::Account(payee.clone()))?; + + Ok(()) + } +} + #[cfg(any(test, feature = "try-runtime"))] impl Pallet { pub(crate) fn do_try_state(_: BlockNumberFor) -> Result<(), TryRuntimeError> { diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 2e5b3aa7b873..bd5372a3e6f2 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -32,7 +32,7 @@ use frame_support::{ }; use frame_system::{ensure_root, ensure_signed, pallet_prelude::*}; use sp_runtime::{ - traits::{CheckedSub, SaturatedConversion, StaticLookup, Zero}, + traits::{SaturatedConversion, StaticLookup, Zero}, ArithmeticError, Perbill, Percent, }; @@ -379,6 +379,16 @@ pub mod pallet { pub type Nominators = CountedStorageMap<_, Twox64Concat, T::AccountId, Nominations>; + /// Nominators whose funds are managed by other pallets. + /// + /// This pallet does not apply any locks on them, therefore they are only virtually bonded. They + /// are expected to be keyless accounts and hence should not be allowed to mutate their ledger + /// directly via this pallet. Instead, these accounts are managed by other pallets and accessed + /// via low level apis. We keep track of them to do minimal integrity checks. + // TODO(ank4n): Can we keep this entry in `Ledger`? Worth a migration? + #[pallet::storage] + pub type VirtualNominators = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; + /// The maximum nominator count before we stop allowing new validators to join. /// /// When this value is not set, no limits are enforced. @@ -858,6 +868,10 @@ pub mod pallet { ControllerDeprecated, /// Cannot reset a ledger. CannotRestoreLedger, + /// Provided reward destination is not allowed. + RewardDestinationRestricted, + /// Not enough funds available to withdraw + NotEnoughFunds, } #[pallet::hooks] @@ -985,29 +999,7 @@ pub mod pallet { #[pallet::compact] max_additional: BalanceOf, ) -> DispatchResult { let stash = ensure_signed(origin)?; - let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?; - - let stash_balance = T::Currency::free_balance(&stash); - if let Some(extra) = stash_balance.checked_sub(&ledger.total) { - let extra = extra.min(max_additional); - ledger.total += extra; - ledger.active += extra; - // Last check: the new active amount of ledger must be more than ED. - ensure!( - ledger.active >= T::Currency::minimum_balance(), - Error::::InsufficientBond - ); - - // NOTE: ledger must be updated prior to calling `Self::weight_of`. - ledger.update()?; - // update this staker in the sorted list, if they exist in it. - if T::VoterList::contains(&stash) { - let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)).defensive(); - } - - Self::deposit_event(Event::::Bonded { stash, amount: extra }); - } - Ok(()) + Self::do_bond_extra(&stash, max_additional) } /// Schedule a portion of the stash to be unlocked ready for transfer out after the bond @@ -1315,9 +1307,7 @@ pub mod pallet { Error::::ControllerDeprecated ); - let _ = ledger - .set_payee(payee) - .defensive_proof("ledger was retrieved from storage, thus its bonded; qed.")?; + ledger.set_payee(payee)?; Ok(()) } diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 709fd1441ec3..19424f57dcdb 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -609,8 +609,13 @@ pub fn do_slash( }; let value = ledger.slash(value, T::Currency::minimum_balance(), slash_era); + if value.is_zero() { + // nothing to do + return + } - if !value.is_zero() { + // Skip slashing for virtual nominators. The pallets managing them should handle the slashing. + if !Pallet::::is_virtual_nominator(stash) { let (imbalance, missing) = T::Currency::slash(stash, value); slashed_imbalance.subsume(imbalance); @@ -618,17 +623,14 @@ pub fn do_slash( // deduct overslash from the reward payout *reward_payout = reward_payout.saturating_sub(missing); } + } - let _ = ledger - .update() - .defensive_proof("ledger fetched from storage so it exists in storage; qed."); + let _ = ledger + .update() + .defensive_proof("ledger fetched from storage so it exists in storage; qed."); - // trigger the event - >::deposit_event(super::Event::::Slashed { - staker: stash.clone(), - amount: value, - }); - } + // trigger the event + >::deposit_event(super::Event::::Slashed { staker: stash.clone(), amount: value }); } /// Apply a previously-unapplied slash. diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 11b7ef41b9a7..3f2c8c9fb493 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -29,7 +29,7 @@ use core::ops::Sub; use scale_info::TypeInfo; use sp_runtime::{ traits::{AtLeast32BitUnsigned, Zero}, - DispatchError, DispatchResult, RuntimeDebug, Saturating, + DispatchError, DispatchResult, Perbill, RuntimeDebug, Saturating, }; pub mod offence; @@ -254,6 +254,9 @@ pub trait StakingInterface { /// schedules have reached their unlocking era should allow more calls to this function. fn unbond(stash: &Self::AccountId, value: Self::Balance) -> DispatchResult; + /// Update the reward destination for the ledger associated with the stash. + fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult; + /// Unlock any funds schedule to unlock before or at the current era. /// /// Returns whether the stash was killed because of this withdraw or not. @@ -274,7 +277,7 @@ pub trait StakingInterface { /// Checks whether an account `staker` has been exposed in an era. fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool; - /// Return the status of the given staker, `None` if not staked at all. + /// Return the status of the given staker, `Err` if not staked at all. fn status(who: &Self::AccountId) -> Result, DispatchError>; /// Checks whether or not this is a validator account. @@ -290,6 +293,9 @@ pub trait StakingInterface { } } + /// Returns the fraction of the slash to be rewarded to reporter. + fn slash_reward_fraction() -> Perbill; + #[cfg(feature = "runtime-benchmarks")] fn max_exposure_page_size() -> Page; @@ -304,6 +310,27 @@ pub trait StakingInterface { fn set_current_era(era: EraIndex); } +/// Set of low level apis to manipulate staking ledger. +/// +/// These apis bypass some or all safety checks and should only be used if you know what you are +/// doing. +pub trait StakingUnsafe: StakingInterface { + /// Release all funds bonded for stake without unbonding the ledger. + /// + /// Unsafe, only used for migration of `nominator` to `virtual_nominator`. + fn force_release(who: &Self::AccountId); + + /// Book-keep a new bond for `who` without applying any locks (hence virtual). + /// + /// It is important that who is a keyless account and therefore cannot interact with staking + /// pallet directly. Caller is responsible for ensuring the passed amount is locked and valid. + fn virtual_bond( + keyless_who: &Self::AccountId, + value: Self::Balance, + payee: &Self::AccountId, + ) -> DispatchResult; +} + /// The amount of exposure for an era that an individual nominator has (susceptible to slashing). #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct IndividualExposure { @@ -422,4 +449,68 @@ pub struct PagedExposureMetadata { pub page_count: Page, } +/// Extension of [`StakingInterface`] with delegation functionality. +/// +/// Introduces two new actors: +/// - `Delegator`: An account that delegates funds to a `Delegatee`. +/// - `Delegatee`: 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. +/// +/// The `Delegatee` is responsible for managing rewards and slashing for all the `Delegators` that +/// have delegated funds to it. +pub trait DelegatedStakeInterface: StakingInterface { + /// Effective balance of the `delegatee` account. + /// + /// This takes into account any pending slashes to `Delegatee`. + fn delegatee_balance(delegatee: &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 `delegatee`. + /// + /// Only used for the initial delegation. Use [`Self::delegate_extra`] to add more delegation. + fn delegate( + delegator: &Self::AccountId, + delegatee: &Self::AccountId, + reward_account: &Self::AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Add more delegation to the `delegatee`. + /// + /// If this is the first delegation, use [`Self::delegate`] instead. + fn delegate_extra( + delegator: &Self::AccountId, + delegatee: &Self::AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Withdraw or revoke delegation to `delegatee`. + /// + /// If there are `delegatee` funds upto `amount` available to withdraw, then those funds would + /// be released to the `delegator` + fn withdraw_delegation( + delegator: &Self::AccountId, + delegatee: &Self::AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Returns true if there are pending slashes posted to the `delegatee` account. + /// + /// Slashes to `delegatee` account are not immediate and are applied lazily. Since `delegatee` + /// has an unbounded number of delegators, immediate slashing is not possible. + fn has_pending_slash(delegatee: &Self::AccountId) -> bool; + + /// Apply a pending slash to a `delegatee` by slashing `value` from `delegator`. + /// + /// If a reporter is provided, the reporter will receive a fraction of the slash as reward. + fn delegator_slash( + delegatee: &Self::AccountId, + delegator: &Self::AccountId, + value: Self::Balance, + maybe_reporter: Option, + ) -> sp_runtime::DispatchResult; +} + sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); From 802784c44f2d3f691f708ba7ba3910f5445bf5e0 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 29 Mar 2024 10:25:28 +0100 Subject: [PATCH 02/85] some comments --- substrate/frame/staking/src/pallet/impls.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index a7428d9ad905..bd890e86468c 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -154,6 +154,8 @@ impl Pallet { pub(super) fn do_bond_extra(stash: &T::AccountId, additional: BalanceOf) -> DispatchResult { let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?; + // for virtual nominators, we don't need to check the balance. Since they are only accessed + // via low level apis, we can assume that the caller has done the due diligence. let extra = if Self::is_virtual_nominator(stash) { additional } else { @@ -1184,10 +1186,15 @@ impl Pallet { } } + /// Whether `who` is a virtual nominator whose funds are managed by another pallet. pub(crate) fn is_virtual_nominator(who: &T::AccountId) -> bool { VirtualNominators::::contains_key(who) } + + /// Update the lock for a staker. + /// + /// For virtual nominators, it is no-op. pub(crate) fn update_lock( who: &T::AccountId, amount: BalanceOf, From d2b680e4b6d337fa39fca748e5a95c44dba5dc8d Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 29 Mar 2024 10:35:25 +0100 Subject: [PATCH 03/85] fix tests --- substrate/frame/nomination-pools/src/mock.rs | 8 ++++++++ substrate/frame/staking/src/pallet/impls.rs | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/substrate/frame/nomination-pools/src/mock.rs b/substrate/frame/nomination-pools/src/mock.rs index 686759604c23..2e30bbeed59e 100644 --- a/substrate/frame/nomination-pools/src/mock.rs +++ b/substrate/frame/nomination-pools/src/mock.rs @@ -128,6 +128,10 @@ impl sp_staking::StakingInterface for StakingMock { Ok(()) } + fn update_payee(_stash: &Self::AccountId, _reward_acc: &Self::AccountId) -> DispatchResult { + unimplemented!("method currently not used in testing") + } + fn chill(_: &Self::AccountId) -> sp_runtime::DispatchResult { Ok(()) } @@ -220,6 +224,10 @@ impl sp_staking::StakingInterface for StakingMock { fn max_exposure_page_size() -> sp_staking::Page { unimplemented!("method currently not used in testing") } + + fn slash_reward_fraction() -> Perbill { + unimplemented!("method currently not used in testing") + } } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index bd890e86468c..aed8eba61383 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1191,7 +1191,6 @@ impl Pallet { VirtualNominators::::contains_key(who) } - /// Update the lock for a staker. /// /// For virtual nominators, it is no-op. From 646c7f4edff8bbd9aee1e4af8cbf865cf1772744 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 21:19:48 +0200 Subject: [PATCH 04/85] tests --- substrate/frame/staking/src/ledger.rs | 11 +-- substrate/frame/staking/src/pallet/impls.rs | 11 ++- substrate/frame/staking/src/pallet/mod.rs | 4 +- substrate/frame/staking/src/tests.rs | 74 +++++++++++++++++++++ substrate/primitives/staking/src/lib.rs | 64 ------------------ 5 files changed, 91 insertions(+), 73 deletions(-) diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs index 641d7c81ae41..2c9fb65925a0 100644 --- a/substrate/frame/staking/src/ledger.rs +++ b/substrate/frame/staking/src/ledger.rs @@ -38,10 +38,7 @@ use frame_support::{ use sp_staking::StakingAccount; use sp_std::prelude::*; -use crate::{ - BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger, - STAKING_ID, -}; +use crate::{BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger, STAKING_ID, VirtualStakers}; #[cfg(any(feature = "runtime-benchmarks", test))] use sp_runtime::traits::Zero; @@ -188,7 +185,9 @@ impl StakingLedger { return Err(Error::::NotStash) } + // update lock on stash based on ledger. Pallet::::update_lock(&self.stash, self.total).map_err(|_| Error::::BadState)?; + Ledger::::insert( &self.controller().ok_or_else(|| { defensive!("update called on a ledger that is not bonded."); @@ -207,6 +206,8 @@ impl StakingLedger { if >::contains_key(&self.stash) { return Err(Error::::AlreadyBonded) } + + // check if the payee is ok. if Pallet::::restrict_reward_destination(&self.stash, payee.clone()) { return Err(Error::::RewardDestinationRestricted); } @@ -222,6 +223,7 @@ impl StakingLedger { return Err(Error::::NotStash) } + // check if the payee is ok. if Pallet::::restrict_reward_destination(&self.stash, payee.clone()) { return Err(Error::::RewardDestinationRestricted); } @@ -265,6 +267,7 @@ impl StakingLedger { >::remove(&stash); >::remove(&stash); + >::remove(&stash); Ok(()) })? diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index aed8eba61383..0a72222e5c5e 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1188,7 +1188,7 @@ impl Pallet { /// Whether `who` is a virtual nominator whose funds are managed by another pallet. pub(crate) fn is_virtual_nominator(who: &T::AccountId) -> bool { - VirtualNominators::::contains_key(who) + VirtualStakers::::contains_key(who) } /// Update the lock for a staker. @@ -1965,10 +1965,14 @@ impl sp_staking::StakingUnsafe for Pallet { return Err(Error::::AlreadyBonded.into()) } + // check if payee not same as who. + ensure!(who != payee, Error::::RewardDestinationRestricted); + + // mark this pallet as consumer of `who`. frame_system::Pallet::::inc_consumers(&who).map_err(|_| Error::::BadState)?; - // mark who as a virtual nominator - VirtualNominators::::insert(who, ()); + // mark who as a virtual nominator. + VirtualStakers::::insert(who, ()); Self::deposit_event(Event::::Bonded { stash: who.clone(), amount: value }); let ledger = StakingLedger::::new(who.clone(), value); @@ -2105,6 +2109,7 @@ impl Pallet { /// * Staking ledger and bond are not corrupted. fn check_ledgers() -> Result<(), TryRuntimeError> { Bonded::::iter() + .filter(|(stash, _)| !VirtualStakers::::contains_key(stash)) .map(|(stash, ctrl)| { // ensure locks consistency. ensure!( diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index bd5372a3e6f2..8f18102175c0 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -379,7 +379,7 @@ pub mod pallet { pub type Nominators = CountedStorageMap<_, Twox64Concat, T::AccountId, Nominations>; - /// Nominators whose funds are managed by other pallets. + /// Stakers whose funds are managed by other pallets. /// /// This pallet does not apply any locks on them, therefore they are only virtually bonded. They /// are expected to be keyless accounts and hence should not be allowed to mutate their ledger @@ -387,7 +387,7 @@ pub mod pallet { /// via low level apis. We keep track of them to do minimal integrity checks. // TODO(ank4n): Can we keep this entry in `Ledger`? Worth a migration? #[pallet::storage] - pub type VirtualNominators = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; + pub type VirtualStakers = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; /// The maximum nominator count before we stop allowing new validators to join. /// diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index a5c9abe2f176..f79586072f1d 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6849,6 +6849,80 @@ mod staking_interface { } } +mod staking_unsafe { + use frame_support::traits::InspectLockableCurrency; + use sp_staking::{StakingUnsafe, StakingInterface, Stake}; + + use super::*; + + #[test] + fn virtual_bond_does_not_lock() { + ExtBuilder::default().build_and_execute(|| { + mock::start_active_era(1); + assert_eq!(Balances::free_balance(10), 1); + // 10 can bond more than its balance amount since we do not require lock for virtual + // bonding. + assert_ok!(::virtual_bond(&10, 100, &15)); + // nothing is locked on 10. + assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0); + // adding more balance does not lock anything as well. + assert_ok!(::bond_extra(&10, 1000)); + // but ledger is updated correctly. + assert_eq!(::stake(&10), Ok(Stake { total: 1100, active: 1100 })); + + // lets try unbonding some amount. + assert_ok!(::unbond(&10, 200)); + assert_eq!( + Staking::ledger(10.into()).unwrap(), + StakingLedgerInspect { + stash: 10, + total: 1100, + active: 1100 - 200, + unlocking: bounded_vec![UnlockChunk { value: 200, era: 1 + 3 }], + legacy_claimed_rewards: bounded_vec![], + }); + + assert_eq!(::stake(&10), Ok(Stake { total: 1100, active: 900 })); + // still no locks. + assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0); + + mock::start_active_era(2); + // cannot withdraw without waiting for unbonding period. + assert_ok!(::withdraw_unbonded(10, 0)); + assert_eq!(::stake(&10), Ok(Stake { total: 1100, active: 900 })); + + // in era 4, 10 can withdraw unlocking amount. + mock::start_active_era(4); + assert_ok!(::withdraw_unbonded(10, 0)); + assert_eq!(::stake(&10), Ok(Stake { total: 900, active: 900 })); + + // unbond all. + assert_ok!(::unbond(&10, 900)); + assert_eq!(::stake(&10), Ok(Stake { total: 900, active: 0 })); + mock::start_active_era(7); + assert_ok!(::withdraw_unbonded(10, 0)); + + // ensure withdrawing all amount cleans up storage. + assert_eq!(Staking::ledger(10.into()), Err(Error::::NotStash)); + assert_eq!(VirtualStakers::::contains_key(10), false); + }) + } + + #[test] + fn virtual_nominator_cannot_pay_reward_to_self_account() { + ExtBuilder::default().build_and_execute(|| { + // cannot set payee to self + assert_noop!(::virtual_bond(&10, 100, &10), Error::::RewardDestinationRestricted); + + // to another account works + assert_ok!(::virtual_bond(&10, 100, &11)); + + // cannot set via set_payee as well. + assert_noop!(::update_payee(&10, &10), Error::::RewardDestinationRestricted); + }); + } +} + mod ledger { use super::*; diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 3f2c8c9fb493..0e1812e10ab2 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -449,68 +449,4 @@ pub struct PagedExposureMetadata { pub page_count: Page, } -/// Extension of [`StakingInterface`] with delegation functionality. -/// -/// Introduces two new actors: -/// - `Delegator`: An account that delegates funds to a `Delegatee`. -/// - `Delegatee`: 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. -/// -/// The `Delegatee` is responsible for managing rewards and slashing for all the `Delegators` that -/// have delegated funds to it. -pub trait DelegatedStakeInterface: StakingInterface { - /// Effective balance of the `delegatee` account. - /// - /// This takes into account any pending slashes to `Delegatee`. - fn delegatee_balance(delegatee: &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 `delegatee`. - /// - /// Only used for the initial delegation. Use [`Self::delegate_extra`] to add more delegation. - fn delegate( - delegator: &Self::AccountId, - delegatee: &Self::AccountId, - reward_account: &Self::AccountId, - amount: Self::Balance, - ) -> DispatchResult; - - /// Add more delegation to the `delegatee`. - /// - /// If this is the first delegation, use [`Self::delegate`] instead. - fn delegate_extra( - delegator: &Self::AccountId, - delegatee: &Self::AccountId, - amount: Self::Balance, - ) -> DispatchResult; - - /// Withdraw or revoke delegation to `delegatee`. - /// - /// If there are `delegatee` funds upto `amount` available to withdraw, then those funds would - /// be released to the `delegator` - fn withdraw_delegation( - delegator: &Self::AccountId, - delegatee: &Self::AccountId, - amount: Self::Balance, - ) -> DispatchResult; - - /// Returns true if there are pending slashes posted to the `delegatee` account. - /// - /// Slashes to `delegatee` account are not immediate and are applied lazily. Since `delegatee` - /// has an unbounded number of delegators, immediate slashing is not possible. - fn has_pending_slash(delegatee: &Self::AccountId) -> bool; - - /// Apply a pending slash to a `delegatee` by slashing `value` from `delegator`. - /// - /// If a reporter is provided, the reporter will receive a fraction of the slash as reward. - fn delegator_slash( - delegatee: &Self::AccountId, - delegator: &Self::AccountId, - value: Self::Balance, - maybe_reporter: Option, - ) -> sp_runtime::DispatchResult; -} - sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); From bc92a19056a1a9496c54b0be5ced3711b9394434 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 21:24:25 +0200 Subject: [PATCH 05/85] fix naming --- substrate/frame/staking/src/pallet/impls.rs | 20 ++++++++++---------- substrate/frame/staking/src/slashing.rs | 4 ++-- substrate/frame/staking/src/tests.rs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 0a72222e5c5e..5fda07fecf4a 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -154,9 +154,9 @@ impl Pallet { pub(super) fn do_bond_extra(stash: &T::AccountId, additional: BalanceOf) -> DispatchResult { let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?; - // for virtual nominators, we don't need to check the balance. Since they are only accessed + // for virtual stakers, we don't need to check the balance. Since they are only accessed // via low level apis, we can assume that the caller has done the due diligence. - let extra = if Self::is_virtual_nominator(stash) { + let extra = if Self::is_virtual_staker(stash) { additional } else { // additional amount or actual balance of stash whichever is lower. @@ -1170,7 +1170,7 @@ impl Pallet { /// Whether the passed reward destination is restricted for the given account. /// - /// Virtual nominators are not allowed to compound their rewards as this pallet does not manage + /// virtual stakers are not allowed to compound their rewards as this pallet does not manage /// locks for them. For external pallets that manage the virtual bond, it is their /// responsibility to distribute the reward and re-bond them. /// @@ -1179,27 +1179,27 @@ impl Pallet { who: &T::AccountId, reward_destination: RewardDestination, ) -> bool { - Self::is_virtual_nominator(who) && + Self::is_virtual_staker(who) && match reward_destination { RewardDestination::Account(payee) => payee == *who, _ => true, } } - /// Whether `who` is a virtual nominator whose funds are managed by another pallet. - pub(crate) fn is_virtual_nominator(who: &T::AccountId) -> bool { + /// Whether `who` is a virtual staker whose funds are managed by another pallet. + pub(crate) fn is_virtual_staker(who: &T::AccountId) -> bool { VirtualStakers::::contains_key(who) } /// Update the lock for a staker. /// - /// For virtual nominators, it is no-op. + /// For virtual stakers, it is no-op. pub(crate) fn update_lock( who: &T::AccountId, amount: BalanceOf, ) -> sp_runtime::DispatchResult { - // Skip locking virtual nominators. They are handled by external pallets. - if !Self::is_virtual_nominator(who) { + // Skip locking virtual stakers. They are handled by external pallets. + if !Self::is_virtual_staker(who) { T::Currency::set_lock( crate::STAKING_ID, who, @@ -1971,7 +1971,7 @@ impl sp_staking::StakingUnsafe for Pallet { // mark this pallet as consumer of `who`. frame_system::Pallet::::inc_consumers(&who).map_err(|_| Error::::BadState)?; - // mark who as a virtual nominator. + // mark who as a virtual staker. VirtualStakers::::insert(who, ()); Self::deposit_event(Event::::Bonded { stash: who.clone(), amount: value }); diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 19424f57dcdb..2011e9eb8301 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -614,8 +614,8 @@ pub fn do_slash( return } - // Skip slashing for virtual nominators. The pallets managing them should handle the slashing. - if !Pallet::::is_virtual_nominator(stash) { + // Skip slashing for virtual stakers. The pallets managing them should handle the slashing. + if !Pallet::::is_virtual_staker(stash) { let (imbalance, missing) = T::Currency::slash(stash, value); slashed_imbalance.subsume(imbalance); diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index f79586072f1d..7d614366d522 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6909,7 +6909,7 @@ mod staking_unsafe { } #[test] - fn virtual_nominator_cannot_pay_reward_to_self_account() { + fn virtual_staker_cannot_pay_reward_to_self_account() { ExtBuilder::default().build_and_execute(|| { // cannot set payee to self assert_noop!(::virtual_bond(&10, 100, &10), Error::::RewardDestinationRestricted); From 2528da740db8b615d3c428cf5c03068a059dc473 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 21:24:51 +0200 Subject: [PATCH 06/85] fmt --- substrate/frame/staking/src/ledger.rs | 5 ++- substrate/frame/staking/src/tests.rs | 54 +++++++++++++++++++-------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs index 2c9fb65925a0..4d3bfe980482 100644 --- a/substrate/frame/staking/src/ledger.rs +++ b/substrate/frame/staking/src/ledger.rs @@ -38,7 +38,10 @@ use frame_support::{ use sp_staking::StakingAccount; use sp_std::prelude::*; -use crate::{BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger, STAKING_ID, VirtualStakers}; +use crate::{ + BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger, + VirtualStakers, STAKING_ID, +}; #[cfg(any(feature = "runtime-benchmarks", test))] use sp_runtime::traits::Zero; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 7d614366d522..bde67760cb0e 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6851,7 +6851,7 @@ mod staking_interface { mod staking_unsafe { use frame_support::traits::InspectLockableCurrency; - use sp_staking::{StakingUnsafe, StakingInterface, Stake}; + use sp_staking::{Stake, StakingInterface, StakingUnsafe}; use super::*; @@ -6868,37 +6868,53 @@ mod staking_unsafe { // adding more balance does not lock anything as well. assert_ok!(::bond_extra(&10, 1000)); // but ledger is updated correctly. - assert_eq!(::stake(&10), Ok(Stake { total: 1100, active: 1100 })); + assert_eq!( + ::stake(&10), + Ok(Stake { total: 1100, active: 1100 }) + ); // lets try unbonding some amount. assert_ok!(::unbond(&10, 200)); assert_eq!( - Staking::ledger(10.into()).unwrap(), - StakingLedgerInspect { - stash: 10, - total: 1100, - active: 1100 - 200, - unlocking: bounded_vec![UnlockChunk { value: 200, era: 1 + 3 }], - legacy_claimed_rewards: bounded_vec![], - }); + Staking::ledger(10.into()).unwrap(), + StakingLedgerInspect { + stash: 10, + total: 1100, + active: 1100 - 200, + unlocking: bounded_vec![UnlockChunk { value: 200, era: 1 + 3 }], + legacy_claimed_rewards: bounded_vec![], + } + ); - assert_eq!(::stake(&10), Ok(Stake { total: 1100, active: 900 })); + assert_eq!( + ::stake(&10), + Ok(Stake { total: 1100, active: 900 }) + ); // still no locks. assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0); mock::start_active_era(2); // cannot withdraw without waiting for unbonding period. assert_ok!(::withdraw_unbonded(10, 0)); - assert_eq!(::stake(&10), Ok(Stake { total: 1100, active: 900 })); + assert_eq!( + ::stake(&10), + Ok(Stake { total: 1100, active: 900 }) + ); // in era 4, 10 can withdraw unlocking amount. mock::start_active_era(4); assert_ok!(::withdraw_unbonded(10, 0)); - assert_eq!(::stake(&10), Ok(Stake { total: 900, active: 900 })); + assert_eq!( + ::stake(&10), + Ok(Stake { total: 900, active: 900 }) + ); // unbond all. assert_ok!(::unbond(&10, 900)); - assert_eq!(::stake(&10), Ok(Stake { total: 900, active: 0 })); + assert_eq!( + ::stake(&10), + Ok(Stake { total: 900, active: 0 }) + ); mock::start_active_era(7); assert_ok!(::withdraw_unbonded(10, 0)); @@ -6912,13 +6928,19 @@ mod staking_unsafe { fn virtual_staker_cannot_pay_reward_to_self_account() { ExtBuilder::default().build_and_execute(|| { // cannot set payee to self - assert_noop!(::virtual_bond(&10, 100, &10), Error::::RewardDestinationRestricted); + assert_noop!( + ::virtual_bond(&10, 100, &10), + Error::::RewardDestinationRestricted + ); // to another account works assert_ok!(::virtual_bond(&10, 100, &11)); // cannot set via set_payee as well. - assert_noop!(::update_payee(&10, &10), Error::::RewardDestinationRestricted); + assert_noop!( + ::update_payee(&10, &10), + Error::::RewardDestinationRestricted + ); }); } } From 0cbec7debe4217eacc9b378d100070056be61208 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sat, 30 Mar 2024 01:38:53 +0100 Subject: [PATCH 07/85] delegated staking without pool tests --- Cargo.lock | 23 + Cargo.toml | 1 + substrate/frame/delegated-staking/Cargo.toml | 70 ++ .../delegated-staking/src/benchmarking.rs | 20 + .../frame/delegated-staking/src/impls.rs | 288 ++++++ substrate/frame/delegated-staking/src/lib.rs | 827 ++++++++++++++++++ substrate/frame/delegated-staking/src/mock.rs | 329 +++++++ .../frame/delegated-staking/src/tests.rs | 541 ++++++++++++ .../frame/delegated-staking/src/types.rs | 314 +++++++ 9 files changed, 2413 insertions(+) create mode 100644 substrate/frame/delegated-staking/Cargo.toml create mode 100644 substrate/frame/delegated-staking/src/benchmarking.rs 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 81eb682a27d7..8555a55d037f 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" 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..2ba000b253d6 --- /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::{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..47c65dc8f471 --- /dev/null +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -0,0 +1,329 @@ +// 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 = 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; +} + +frame_support::construct_runtime!( + pub enum Runtime { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + Staking: pallet_staking, + DelegatedStaking: delegated_staking, + } +); + +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(|| { + 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 ObservedEventsDelegatedStaking: usize = 0; +} + +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..790e96fbe692 --- /dev/null +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -0,0 +1,541 @@ +// 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; + +#[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 + ); + }); + } +} \ No newline at end of file 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, +} + +impl DelegateeLedger { + /// Create a new instance of `DelegateeLedger`. + pub(crate) fn new(reward_destination: &T::AccountId) -> Self { + DelegateeLedger { + payee: reward_destination.clone(), + total_delegated: Zero::zero(), + unclaimed_withdrawals: Zero::zero(), + pending_slash: Zero::zero(), + } + } + + /// Get `DelegateeLedger` from storage. + pub(crate) fn get(key: &T::AccountId) -> Option { + >::get(key) + } + + /// Save self to storage with the given key. + pub(crate) fn save(self, key: &T::AccountId) { + >::insert(key, self) + } + + /// Effective total balance of the `delegatee`. + /// + /// This takes into account any slashes reported to `Delegatee` 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) + } + + /// Delegatee 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 `DelegateeLedger` to provide additional functionality. +#[derive(Clone)] +pub struct Delegatee { + /// storage key + pub key: T::AccountId, + /// storage value + pub ledger: DelegateeLedger, +} + +impl Delegatee { + /// Get `Delegatee` from storage if it exists or return an error. + pub(crate) fn from(delegatee: &T::AccountId) -> Result, DispatchError> { + let ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotDelegatee)?; + Ok(Delegatee { key: delegatee.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(Delegatee { + ledger: DelegateeLedger { + 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(Delegatee { + ledger: DelegateeLedger { + 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 delegatee balance" + ); + + stakeable.saturating_sub(bonded_stake) + } + + /// Remove slashes from the `DelegateeLedger`. + 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); + + Delegatee { + ledger: DelegateeLedger { pending_slash, total_delegated, ..self.ledger }, + ..self + } + } + + /// Get the total stake of delegatee 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 delegatee 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 delegatee. + 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.save(&key) + } + + /// Save self and remove if no delegation left. + /// + /// Returns error if the delegate is in an unexpected state. + pub(crate) fn save_or_kill(self) -> Result<(), DispatchError> { + 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); + } else { + self.ledger.save(&key) + } + + Ok(()) + } + + /// Reloads self from storage. + #[cfg(test)] + pub(crate) fn refresh(&self) -> Result, DispatchError> { + Self::from(&self.key) + } + + /// Balance of `Delegatee` that is not bonded. + /// + /// This is similar to [Self::available_to_bond] except it also includes `unclaimed_withdrawals` + /// of `Delegatee`. + #[cfg(test)] + pub(crate) fn total_unbonded(&self) -> BalanceOf { + let bonded_stake = self.bonded_stake(); + + let net_balance = self.ledger.effective_balance(); + + defensive_assert!( + net_balance >= bonded_stake, + "cannot be bonded with more than the delegatee balance" + ); + + net_balance.saturating_sub(bonded_stake) + } +} From e6152ed5eb7ac7ade5922adf63de60273c730a7b Mon Sep 17 00:00:00 2001 From: Ankan Date: Sat, 30 Mar 2024 02:08:12 +0100 Subject: [PATCH 08/85] fix clippy errors --- .../frame/delegated-staking/src/impls.rs | 8 ++++---- substrate/frame/delegated-staking/src/lib.rs | 20 +++++++++---------- substrate/frame/delegated-staking/src/mock.rs | 16 ++++++--------- .../frame/delegated-staking/src/tests.rs | 6 ++---- .../frame/delegated-staking/src/types.rs | 2 ++ 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 2ba000b253d6..b188db4aa5f9 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -54,7 +54,7 @@ impl StakingInterface for Pallet { fn stake(who: &Self::AccountId) -> Result, DispatchError> { ensure!(Self::is_delegatee(who), Error::::NotSupported); - return T::CoreStaking::stake(who); + T::CoreStaking::stake(who) } fn total_stake(who: &Self::AccountId) -> Result { @@ -80,7 +80,7 @@ impl StakingInterface for Pallet { fn fully_unbond(who: &Self::AccountId) -> DispatchResult { ensure!(Self::is_delegatee(who), Error::::NotSupported); - return T::CoreStaking::fully_unbond(who); + T::CoreStaking::fully_unbond(who) } fn bond( @@ -100,12 +100,12 @@ impl StakingInterface for Pallet { fn nominate(who: &Self::AccountId, validators: Vec) -> DispatchResult { ensure!(Self::is_delegatee(who), Error::::NotDelegatee); - return T::CoreStaking::nominate(who, validators); + T::CoreStaking::nominate(who, validators) } fn chill(who: &Self::AccountId) -> DispatchResult { ensure!(Self::is_delegatee(who), Error::::NotDelegatee); - return T::CoreStaking::chill(who); + T::CoreStaking::chill(who) } fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 3b49ff477f27..d2097bb49cb3 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -54,9 +54,9 @@ //! - **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. +//! such as their total delegated stake. //! - **Delegation**: A data structure that stores the amount of funds delegated to a `delegatee` by -//! a `delegator`. +//! a `delegator`. //! //! ## Interface //! @@ -129,7 +129,7 @@ //! ## 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. +//! 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)] @@ -469,7 +469,7 @@ impl Pallet { /// 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() + T::CoreStaking::status(who).is_err() } /// Returns true if who is a [`StakerStatus::Nominator`] on [`Config::CoreStaking`]. @@ -531,7 +531,7 @@ impl Pallet { if delegatee.is_bonded() { T::CoreStaking::bond_extra(&delegatee.key, amount) } else { - T::CoreStaking::virtual_bond(&delegatee.key, amount, &delegatee.reward_account()) + T::CoreStaking::virtual_bond(&delegatee.key, amount, delegatee.reward_account()) } } @@ -605,7 +605,7 @@ impl Pallet { let released = T::Currency::release( &HoldReason::Delegating.into(), - &delegator, + delegator, amount, Precision::BestEffort, )?; @@ -658,7 +658,7 @@ impl Pallet { amount: BalanceOf, ) -> DispatchResult { let source_delegation = - Delegators::::get(&source_delegator).defensive_ok_or(Error::::BadState)?; + 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); @@ -679,7 +679,7 @@ impl Pallet { // release funds from source let released = T::Currency::release( &HoldReason::Delegating.into(), - &source_delegator, + source_delegator, amount, Precision::BestEffort, )?; @@ -689,7 +689,7 @@ impl Pallet { // 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, + source_delegator, destination_delegator, amount, Preservation::Preserve, @@ -697,7 +697,7 @@ impl Pallet { .map_err(|_| Error::::BadState)?; // hold the funds again in the new delegator account. - T::Currency::hold(&HoldReason::Delegating.into(), &destination_delegator, amount)?; + T::Currency::hold(&HoldReason::Delegating.into(), destination_delegator, amount)?; Ok(()) } diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 47c65dc8f471..84854d6864d6 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -176,14 +176,9 @@ frame_support::construct_runtime!( } ); +#[derive(Default)] pub struct ExtBuilder {} -impl Default for ExtBuilder { - fn default() -> Self { - Self {} - } -} - impl ExtBuilder { fn build(self) -> sp_io::TestExternalities { sp_tracing::try_init_simple(); @@ -242,7 +237,7 @@ impl ExtBuilder { ext } - pub fn build_and_execute(self, test: impl FnOnce() -> ()) { + pub fn build_and_execute(self, test: impl FnOnce()) { sp_tracing::try_init_simple(); let mut ext = self.build(); ext.execute_with(test); @@ -280,7 +275,7 @@ pub(crate) fn setup_delegation_stake( fund(delegator, amount_to_delegate + ExistentialDeposit::get()); assert_ok!(DelegatedStaking::delegate_funds( - RawOrigin::Signed(delegator.clone()).into(), + RawOrigin::Signed(*delegator).into(), delegatee, amount_to_delegate )); @@ -306,15 +301,16 @@ pub(crate) fn get_delegatee(delegatee: &AccountId) -> Delegatee { Delegatee::::from(delegatee).expect("delegate should exist") } - +#[allow(unused)] pub(crate) fn held_balance(who: &AccountId) -> Balance { - Balances::balance_on_hold(&HoldReason::Delegating.into(), &who) + Balances::balance_on_hold(&HoldReason::Delegating.into(), who) } parameter_types! { static ObservedEventsDelegatedStaking: usize = 0; } +#[allow(unused)] pub(crate) fn events_since_last_call() -> Vec> { let events = System::events() .into_iter() diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 790e96fbe692..844ab74bfa09 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -198,9 +198,7 @@ fn delegatee_restrictions() { #[test] fn apply_pending_slash() { ExtBuilder::default().build_and_execute(|| { - ( - // fixme(ank4n): add tests for apply_pending_slash - ) + // fixme(ank4n): add tests for apply_pending_slash }); } @@ -538,4 +536,4 @@ mod staking_integration { ); }); } -} \ No newline at end of file +} diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 0dcdcc0659a4..bd971f39cde6 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -290,6 +290,7 @@ impl Delegatee { /// Reloads self from storage. #[cfg(test)] + #[allow(unused)] pub(crate) fn refresh(&self) -> Result, DispatchError> { Self::from(&self.key) } @@ -299,6 +300,7 @@ impl Delegatee { /// This is similar to [Self::available_to_bond] except it also includes `unclaimed_withdrawals` /// of `Delegatee`. #[cfg(test)] + #[allow(unused)] pub(crate) fn total_unbonded(&self) -> BalanceOf { let bonded_stake = self.bonded_stake(); From a91a417823893042d2e9af8c05ae4d2faf278aaa Mon Sep 17 00:00:00 2001 From: Ankan Date: Sat, 30 Mar 2024 02:12:01 +0100 Subject: [PATCH 09/85] remove pool dependency --- Cargo.lock | 1 - substrate/frame/delegated-staking/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8555a55d037f..fd82e1711fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9706,7 +9706,6 @@ dependencies = [ "frame-support", "frame-system", "pallet-balances", - "pallet-nomination-pools", "pallet-staking", "pallet-staking-reward-curve", "pallet-timestamp", diff --git a/substrate/frame/delegated-staking/Cargo.toml b/substrate/frame/delegated-staking/Cargo.toml index b4b9768256c6..b43d09692db5 100644 --- a/substrate/frame/delegated-staking/Cargo.toml +++ b/substrate/frame/delegated-staking/Cargo.toml @@ -26,7 +26,6 @@ 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" } From d6069a42c465ff282df8fd9bc1a29e5a10d8aaaa Mon Sep 17 00:00:00 2001 From: Ankan Date: Sat, 30 Mar 2024 02:15:55 +0100 Subject: [PATCH 10/85] fix imports --- substrate/frame/delegated-staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index d2097bb49cb3..84271f0246ed 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -435,7 +435,7 @@ pub mod pallet { #[pallet::hooks] impl Hooks> for Pallet { #[cfg(feature = "try-runtime")] - fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> { + fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { Self::do_try_state() } From 3e379167c41ae6f4d328272b20aebfbdab27aa4e Mon Sep 17 00:00:00 2001 From: Ankan Date: Sat, 30 Mar 2024 02:28:18 +0100 Subject: [PATCH 11/85] taplo fmt toml files --- substrate/frame/delegated-staking/Cargo.toml | 68 ++++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/substrate/frame/delegated-staking/Cargo.toml b/substrate/frame/delegated-staking/Cargo.toml index b43d09692db5..cecaefbe779f 100644 --- a/substrate/frame/delegated-staking/Cargo.toml +++ b/substrate/frame/delegated-staking/Cargo.toml @@ -13,11 +13,11 @@ 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} +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-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] @@ -29,41 +29,41 @@ 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} +frame-election-provider-support = { path = "../election-provider-support", default-features = false } [features] -default = [ "std" ] +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", + "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-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", + "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-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", + "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", ] From 79c4a8607f2179206b3796dcc4ab2978c8a9d124 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sat, 30 Mar 2024 02:30:38 +0100 Subject: [PATCH 12/85] taplo refmt --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2966e45fbec0..f9f89d313175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -317,6 +317,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", @@ -385,7 +386,6 @@ 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", From 35c5e804de7817ee8e3c783aef359c71e3cd6841 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 18:46:28 +0200 Subject: [PATCH 13/85] remove calls --- .../frame/delegated-staking/src/impls.rs | 22 +-- substrate/frame/delegated-staking/src/lib.rs | 166 +++++++++--------- substrate/frame/delegated-staking/src/mock.rs | 4 +- .../frame/delegated-staking/src/tests.rs | 94 +++++----- .../frame/delegated-staking/src/types.rs | 2 +- 5 files changed, 141 insertions(+), 147 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index b188db4aa5f9..e09a4b373834 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -53,12 +53,12 @@ impl StakingInterface for Pallet { } fn stake(who: &Self::AccountId) -> Result, DispatchError> { - ensure!(Self::is_delegatee(who), Error::::NotSupported); + ensure!(Self::is_agent(who), Error::::NotSupported); T::CoreStaking::stake(who) } fn total_stake(who: &Self::AccountId) -> Result { - if Self::is_delegatee(who) { + if Self::is_agent(who) { return T::CoreStaking::total_stake(who); } @@ -79,7 +79,7 @@ impl StakingInterface for Pallet { } fn fully_unbond(who: &Self::AccountId) -> DispatchResult { - ensure!(Self::is_delegatee(who), Error::::NotSupported); + ensure!(Self::is_agent(who), Error::::NotSupported); T::CoreStaking::fully_unbond(who) } @@ -99,17 +99,17 @@ impl StakingInterface for Pallet { } fn nominate(who: &Self::AccountId, validators: Vec) -> DispatchResult { - ensure!(Self::is_delegatee(who), Error::::NotDelegatee); + ensure!(Self::is_agent(who), Error::::NotAgent); T::CoreStaking::nominate(who, validators) } fn chill(who: &Self::AccountId) -> DispatchResult { - ensure!(Self::is_delegatee(who), Error::::NotDelegatee); + ensure!(Self::is_agent(who), Error::::NotAgent); T::CoreStaking::chill(who) } fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { - let ledger = >::get(who).ok_or(Error::::NotDelegatee)?; + let ledger = >::get(who).ok_or(Error::::NotAgent)?; ensure!(ledger.stakeable_balance() >= extra, Error::::NotEnoughFunds); T::CoreStaking::bond_extra(who, extra) @@ -154,7 +154,7 @@ impl StakingInterface for Pallet { } fn status(who: &Self::AccountId) -> Result, DispatchError> { - ensure!(Self::is_delegatee(who), Error::::NotDelegatee); + ensure!(Self::is_agent(who), Error::::NotAgent); T::CoreStaking::status(who) } @@ -209,13 +209,13 @@ impl DelegatedStakeInterface for Pallet { reward_account: &Self::AccountId, amount: Self::Balance, ) -> DispatchResult { - Pallet::::register_as_delegatee( + Pallet::::register_agent( RawOrigin::Signed(delegatee.clone()).into(), reward_account.clone(), )?; // Delegate the funds from who to the delegatee account. - Pallet::::delegate_funds( + Pallet::::delegate_to_agent( RawOrigin::Signed(who.clone()).into(), delegatee.clone(), amount, @@ -228,7 +228,7 @@ impl DelegatedStakeInterface for Pallet { delegatee: &Self::AccountId, amount: Self::Balance, ) -> DispatchResult { - Pallet::::delegate_funds( + Pallet::::delegate_to_agent( RawOrigin::Signed(who.clone()).into(), delegatee.clone(), amount, @@ -245,7 +245,7 @@ impl DelegatedStakeInterface for Pallet { amount: Self::Balance, ) -> DispatchResult { // fixme(ank4n): Can this not require slashing spans? - Pallet::::release( + Pallet::::release_delegation( RawOrigin::Signed(delegatee.clone()).into(), delegator.clone(), amount, diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 84271f0246ed..fcf633af85f5 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -162,7 +162,6 @@ use frame_support::{ tokens::{fungible::Credit, Fortitude, Precision, Preservation}, Defensive, DefensiveOption, Imbalance, OnUnbalanced, }, - weights::Weight, }; use sp_runtime::{ @@ -225,7 +224,7 @@ pub mod pallet { /// The account does not have enough funds to perform the operation. NotEnoughFunds, /// Not an existing delegatee account. - NotDelegatee, + NotAgent, /// Not a Delegator account. NotDelegator, /// Some corruption in internal state. @@ -270,37 +269,44 @@ pub mod pallet { pub(crate) type Delegatees = CountedStorageMap<_, Twox64Concat, T::AccountId, DelegateeLedger, OptionQuery>; - #[pallet::call] + // 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 be a `Delegatee`. + /// Register an account to become a stake `Agent`. Sometimes also called 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( + /// 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`]. + /// + /// Implementation note: This function allows any account to become an agent. It is + /// important though that accounts that call [`StakingUnsafe::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 `delegatee` cannot register again. - ensure!(!Self::is_delegatee(&who), Error::::NotAllowed); + // Existing `agent` cannot register again. + ensure!(!Self::is_agent(&who), Error::::NotAllowed); - // A delegator cannot become a `delegatee`. + // A delegator cannot become an `agent`. 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. + // Reward account cannot be same as `agent` account. ensure!(reward_account != who, Error::::InvalidRewardDestination); - Self::do_register_delegatee(&who, &reward_account); + Self::do_register_agent(&who, &reward_account); Ok(()) } - /// Migrate from a `Nominator` account to `Delegatee` account. + /// Migrate from a `Nominator` account to `Agent` account. /// /// The origin needs to /// - be a `Nominator` with `CoreStaking`, @@ -308,39 +314,35 @@ pub mod pallet { /// - 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 + /// 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. - #[pallet::call_index(1)] - #[pallet::weight(Weight::default())] - pub fn migrate_to_delegatee( + pub fn migrate_to_agent( 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); + // ensure who is not already an agent. + ensure!(!Self::is_agent(&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. + // Reward account cannot be same as `agent` account. ensure!(reward_account != who, Error::::InvalidRewardDestination); - Self::do_migrate_to_delegatee(&who, &reward_account) + Self::do_migrate_to_agent(&who, &reward_account) } - /// Release delegated amount to delegator. + /// Release previously delegated funds by delegator to origin. /// - /// This can be called by existing `delegatee` accounts. + /// Only agents can call this. /// - /// Tries to withdraw unbonded fund from `CoreStaking` if needed and release amount to + /// Tries to withdraw unbonded funds from `CoreStaking` if needed and release amount to /// `delegator`. - #[pallet::call_index(2)] - #[pallet::weight(Weight::default())] - pub fn release( + pub fn release_delegation( origin: OriginFor, delegator: T::AccountId, amount: BalanceOf, @@ -350,85 +352,81 @@ pub mod pallet { Self::do_release(&who, &delegator, amount, num_slashing_spans) } - /// Migrate delegated fund. + /// Claim delegated funds that are held in `proxy_delegator` to the claiming delegator's + /// account. If successful, the specified funds will be delegated directly from `delegator` + /// account to the agent. /// - /// This can be called by migrating `delegatee` accounts. + /// This can be called by `agent` accounts that were previously a direct `Nominator` with + /// [`Config::CoreStaking`] and has some remaining unclaimed delegations. /// - /// This moves delegator funds from `pxoxy_delegator` account to `delegator` account. - #[pallet::call_index(3)] - #[pallet::weight(Weight::default())] - pub fn migrate_delegation( + /// Internally, it moves some delegations from `pxoxy_delegator` account to `delegator` + /// account and reapplying the holds. + pub fn claim_delegation( origin: OriginFor, delegator: T::AccountId, amount: BalanceOf, ) -> DispatchResult { - let delegatee = ensure_signed(origin)?; + 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_delegatee(&delegator), Error::::NotAllowed); + ensure!(!Self::is_agent(&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); + ensure!(Self::is_agent(&agent), Error::::NotAgent); // and has enough delegated balance to migrate. - let proxy_delegator = Self::sub_account(AccountType::ProxyDelegator, delegatee); + 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 funds to a `Delegatee` account and bonds it to [Config::CoreStaking]. + /// 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. /// - /// If delegation already exists, it increases the delegation by `amount`. - #[pallet::call_index(4)] - #[pallet::weight(Weight::default())] - pub fn delegate_funds( + /// 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, - delegatee: T::AccountId, + agent: T::AccountId, amount: BalanceOf, ) -> DispatchResult { - let who = ensure_signed(origin)?; + let delegator = 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!( + Delegation::::can_delegate(&delegator, &agent), + Error::::InvalidDelegation + ); + ensure!(Self::not_direct_staker(&delegator), Error::::AlreadyStaking); - // ensure delegatee is sane. - ensure!(Self::is_delegatee(&delegatee), Error::::NotDelegatee); + // ensure agent is sane. + ensure!(Self::is_agent(&agent), Error::::NotAgent); - let delegator_balance = - T::Currency::reducible_balance(&who, Preservation::Preserve, Fortitude::Polite); + let delegator_balance = T::Currency::reducible_balance( + &delegator, + 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) - } + Self::do_delegate(&delegator, &agent, 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) + // bond the newly delegated amount to `CoreStaking`. + Self::do_bond(&agent, amount) } } @@ -458,7 +456,7 @@ impl Pallet { } /// Returns true if who is registered as a `Delegatee`. - fn is_delegatee(who: &T::AccountId) -> bool { + fn is_agent(who: &T::AccountId) -> bool { >::contains_key(who) } @@ -479,7 +477,7 @@ impl Pallet { .unwrap_or(false) } - fn do_register_delegatee(who: &T::AccountId, reward_account: &T::AccountId) { + fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) { DelegateeLedger::::new(reward_account).save(who); // Delegatee is a virtual account. Make this account exist. @@ -488,10 +486,7 @@ impl Pallet { frame_system::Pallet::::inc_providers(who); } - fn do_migrate_to_delegatee( - who: &T::AccountId, - reward_account: &T::AccountId, - ) -> DispatchResult { + fn do_migrate_to_agent(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()); @@ -516,7 +511,7 @@ impl Pallet { T::Currency::transfer(who, &proxy_delegator, stake.total, Preservation::Protect) .map_err(|_| Error::::BadState)?; - Self::do_register_delegatee(who, reward_account); + Self::do_register_agent(who, reward_account); T::CoreStaking::update_payee(who, reward_account)?; Self::do_delegate(&proxy_delegator, who, stake.total) @@ -540,7 +535,7 @@ impl Pallet { delegatee: &T::AccountId, amount: BalanceOf, ) -> DispatchResult { - let mut ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotDelegatee)?; + let mut ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotAgent)?; let new_delegation_amount = if let Some(existing_delegation) = Delegation::::get(delegator) { @@ -579,7 +574,7 @@ impl Pallet { 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.delegatee == who, Error::::NotAgent); ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); // if we do not already have enough funds to be claimed, try withdraw some more. @@ -663,8 +658,7 @@ impl Pallet { // 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) + !Self::is_delegator(destination_delegator) && !Self::is_agent(destination_delegator) ); // update delegations @@ -711,7 +705,7 @@ impl Pallet { let delegatee = Delegatee::::from(&delegatee_acc)?; let delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?; - ensure!(delegation.delegatee == delegatee_acc, Error::::NotDelegatee); + ensure!(delegation.delegatee == delegatee_acc, Error::::NotAgent); ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); let (mut credit, missing) = @@ -804,7 +798,7 @@ impl Pallet { T::CoreStaking::status(delegator).is_err(), "delegator should not be directly staked" ); - ensure!(!Self::is_delegatee(delegator), "delegator cannot be delegatee"); + ensure!(!Self::is_agent(delegator), "delegator cannot be delegatee"); delegation_aggregation .entry(delegation.delegatee.clone()) diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 84854d6864d6..1a4f079735b7 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -264,7 +264,7 @@ pub(crate) fn setup_delegation_stake( increment: Balance, ) -> Balance { fund(&delegatee, 100); - assert_ok!(DelegatedStaking::register_as_delegatee( + assert_ok!(DelegatedStaking::register_agent( RawOrigin::Signed(delegatee).into(), reward_acc )); @@ -274,7 +274,7 @@ pub(crate) fn setup_delegation_stake( delegated_amount += amount_to_delegate; fund(delegator, amount_to_delegate + ExistentialDeposit::get()); - assert_ok!(DelegatedStaking::delegate_funds( + assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(*delegator).into(), delegatee, amount_to_delegate diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 844ab74bfa09..9b9dd92bc227 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -31,21 +31,21 @@ fn create_a_delegatee_with_first_delegator() { // set intention to accept delegation. fund(&delegatee, 1000); - assert_ok!(DelegatedStaking::register_as_delegatee( + assert_ok!(DelegatedStaking::register_agent( RawOrigin::Signed(delegatee).into(), reward_account )); // delegate to this account fund(&delegator, 1000); - assert_ok!(DelegatedStaking::delegate_funds( + assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator).into(), delegatee, 100 )); // verify - assert!(DelegatedStaking::is_delegatee(&delegatee)); + assert!(DelegatedStaking::is_agent(&delegatee)); assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 100); assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator), 100); }); @@ -56,13 +56,13 @@ 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), + DelegatedStaking::register_agent(RawOrigin::Signed(100).into(), 100), Error::::InvalidRewardDestination ); // an existing validator cannot become delegatee assert_noop!( - DelegatedStaking::register_as_delegatee( + DelegatedStaking::register_agent( RawOrigin::Signed(mock::GENESIS_VALIDATOR).into(), 100 ), @@ -71,14 +71,14 @@ fn cannot_become_delegatee() { // an existing nominator cannot become delegatee assert_noop!( - DelegatedStaking::register_as_delegatee( + DelegatedStaking::register_agent( RawOrigin::Signed(mock::GENESIS_NOMINATOR_ONE).into(), 100 ), Error::::AlreadyStaking ); assert_noop!( - DelegatedStaking::register_as_delegatee( + DelegatedStaking::register_agent( RawOrigin::Signed(mock::GENESIS_NOMINATOR_TWO).into(), 100 ), @@ -95,11 +95,11 @@ fn create_multiple_delegators() { // stakeable balance is 0 for non delegatee fund(&delegatee, 1000); - assert!(!DelegatedStaking::is_delegatee(&delegatee)); + assert!(!DelegatedStaking::is_agent(&delegatee)); assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 0); // set intention to accept delegation. - assert_ok!(DelegatedStaking::register_as_delegatee( + assert_ok!(DelegatedStaking::register_agent( RawOrigin::Signed(delegatee).into(), reward_account )); @@ -107,7 +107,7 @@ fn create_multiple_delegators() { // create 100 delegators for i in 202..302 { fund(&i, 100 + ExistentialDeposit::get()); - assert_ok!(DelegatedStaking::delegate_funds( + assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(i).into(), delegatee, 100 @@ -117,7 +117,7 @@ fn create_multiple_delegators() { } // verify - assert!(DelegatedStaking::is_delegatee(&delegatee)); + assert!(DelegatedStaking::is_agent(&delegatee)); assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 100 * 100); }); } @@ -129,12 +129,12 @@ fn delegatee_restrictions() { let delegatee_one = 200; let delegator_one = 210; fund(&delegatee_one, 100); - assert_ok!(DelegatedStaking::register_as_delegatee( + assert_ok!(DelegatedStaking::register_agent( RawOrigin::Signed(delegatee_one).into(), delegatee_one + 1 )); fund(&delegator_one, 200); - assert_ok!(DelegatedStaking::delegate_funds( + assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator_one).into(), delegatee_one, 100 @@ -143,12 +143,12 @@ fn delegatee_restrictions() { let delegatee_two = 300; let delegator_two = 310; fund(&delegatee_two, 100); - assert_ok!(DelegatedStaking::register_as_delegatee( + assert_ok!(DelegatedStaking::register_agent( RawOrigin::Signed(delegatee_two).into(), delegatee_two + 1 )); fund(&delegator_two, 200); - assert_ok!(DelegatedStaking::delegate_funds( + assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator_two).into(), delegatee_two, 100 @@ -156,7 +156,7 @@ fn delegatee_restrictions() { // delegatee one tries to delegate to delegatee 2 assert_noop!( - DelegatedStaking::delegate_funds( + DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegatee_one).into(), delegatee_two, 10 @@ -166,7 +166,7 @@ fn delegatee_restrictions() { // delegatee one tries to delegate to a delegator assert_noop!( - DelegatedStaking::delegate_funds( + DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegatee_one).into(), delegator_one, 10 @@ -174,7 +174,7 @@ fn delegatee_restrictions() { Error::::InvalidDelegation ); assert_noop!( - DelegatedStaking::delegate_funds( + DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegatee_one).into(), delegator_two, 10 @@ -185,7 +185,7 @@ fn delegatee_restrictions() { // delegator one tries to delegate to delegatee 2 as well (it already delegates to delegatee // 1) assert_noop!( - DelegatedStaking::delegate_funds( + DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator_one).into(), delegatee_two, 10 @@ -217,7 +217,7 @@ mod staking_integration { // set intention to become a delegatee fund(&delegatee, 100); - assert_ok!(DelegatedStaking::register_as_delegatee( + assert_ok!(DelegatedStaking::register_agent( RawOrigin::Signed(delegatee).into(), reward_acc )); @@ -228,7 +228,7 @@ mod staking_integration { // set some delegations for delegator in 200..250 { fund(&delegator, 200); - assert_ok!(DelegatedStaking::delegate_funds( + assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator).into(), delegatee, 100 @@ -269,10 +269,10 @@ mod staking_integration { assert!(eq_stake(delegatee, total_staked, total_staked)); // Withdrawing without unbonding would fail. assert_noop!( - DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 301, 50, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 301, 50, 0), Error::::NotEnoughFunds ); - // assert_noop!(DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 200, 50, + // assert_noop!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 200, 50, // 0), Error::::NotAllowed); active and total stake remains same assert!(eq_stake(delegatee, total_staked, total_staked)); @@ -291,7 +291,7 @@ mod staking_integration { // nothing to withdraw at era 4 assert_noop!( - DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 305, 50, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 305, 50, 0), Error::::NotEnoughFunds ); @@ -303,43 +303,43 @@ mod staking_integration { start_era(5); // at era 5, 50 tokens are withdrawable, cannot withdraw more. assert_noop!( - DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 305, 51, 0), + DelegatedStaking::release_delegation(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)); + assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 305, 30, 0)); + assert_ok!(DelegatedStaking::release_delegation(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), + DelegatedStaking::release_delegation(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), + DelegatedStaking::release_delegation(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), + DelegatedStaking::release_delegation(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)); + assert_ok!(DelegatedStaking::release_delegation(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)); + assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 320, 200, 0)); // cannot withdraw anything more.. assert_noop!( - DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 301, 1, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 301, 1, 0), Error::::NotEnoughFunds ); assert_noop!( - DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 350, 1, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 350, 1, 0), Error::::NotEnoughFunds ); }); @@ -353,7 +353,7 @@ mod staking_integration { // verify withdraw not possible yet assert_noop!( - DelegatedStaking::release(RawOrigin::Signed(delegatee).into(), 300, 100, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 300, 100, 0), Error::::NotEnoughFunds ); @@ -363,14 +363,14 @@ mod staking_integration { // withdrawn and test its claimed from there first. // fund(&300, 1000); - // assert_ok!(DelegatedStaking::delegate_funds(RawOrigin::Signed(300.into()), delegate, + // assert_ok!(DelegatedStaking::delegate_to_agent(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, + // assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 300, 100, // 0)); assert_eq!(get_delegatee(&delegatee).available_to_bond(), 0); }); } @@ -384,15 +384,15 @@ mod staking_integration { // `delegatee` account cannot be reward destination assert_noop!( - DelegatedStaking::register_as_delegatee(RawOrigin::Signed(200).into(), 200), + DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 200), Error::::InvalidRewardDestination ); // different reward account works - assert_ok!(DelegatedStaking::register_as_delegatee(RawOrigin::Signed(200).into(), 201)); + assert_ok!(DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 201)); // add some delegations to it fund(&300, 1000); - assert_ok!(DelegatedStaking::delegate_funds(RawOrigin::Signed(300).into(), 200, 100)); + assert_ok!(DelegatedStaking::delegate_to_agent(RawOrigin::Signed(300).into(), 200, 100)); // if delegate calls Staking pallet directly with a different reward destination, it // fails. @@ -424,24 +424,24 @@ mod staking_integration { // Registering again is noop assert_noop!( - DelegatedStaking::register_as_delegatee(RawOrigin::Signed(200).into(), 201), + DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 201), Error::::NotAllowed ); // a delegator cannot become delegate assert_noop!( - DelegatedStaking::register_as_delegatee(RawOrigin::Signed(202).into(), 203), + DelegatedStaking::register_agent(RawOrigin::Signed(202).into(), 203), Error::::NotAllowed ); // existing staker cannot become a delegate assert_noop!( - DelegatedStaking::register_as_delegatee( + DelegatedStaking::register_agent( RawOrigin::Signed(GENESIS_NOMINATOR_ONE).into(), 201 ), Error::::AlreadyStaking ); assert_noop!( - DelegatedStaking::register_as_delegatee( + DelegatedStaking::register_agent( RawOrigin::Signed(GENESIS_VALIDATOR).into(), 201 ), @@ -484,7 +484,7 @@ mod staking_integration { // with at least ED. let proxy_delegator = DelegatedStaking::sub_account(AccountType::ProxyDelegator, 200); - assert_ok!(DelegatedStaking::migrate_to_delegatee(RawOrigin::Signed(200).into(), 201)); + assert_ok!(DelegatedStaking::migrate_to_agent(RawOrigin::Signed(200).into(), 201)); // verify all went well let mut expected_proxy_delegated_amount = staked_amount; @@ -508,7 +508,7 @@ mod staking_integration { // fund them with ED fund(&delegator, ExistentialDeposit::get()); // migrate 1/4th amount into each delegator - assert_ok!(DelegatedStaking::migrate_delegation( + assert_ok!(DelegatedStaking::claim_delegation( RawOrigin::Signed(200).into(), delegator, delegator_share @@ -531,7 +531,7 @@ mod staking_integration { // cannot use migrate delegator anymore assert_noop!( - DelegatedStaking::migrate_delegation(RawOrigin::Signed(200).into(), 305, 1), + DelegatedStaking::claim_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 index bd971f39cde6..494a616e76f8 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -167,7 +167,7 @@ pub struct Delegatee { impl Delegatee { /// Get `Delegatee` from storage if it exists or return an error. pub(crate) fn from(delegatee: &T::AccountId) -> Result, DispatchError> { - let ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotDelegatee)?; + let ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotAgent)?; Ok(Delegatee { key: delegatee.clone(), ledger }) } From c5a5d32431d27bd893562726e0f226b9b89cf88d Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 19:24:16 +0200 Subject: [PATCH 14/85] fix docs --- substrate/frame/delegated-staking/src/lib.rs | 119 +++++++++---------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index fcf633af85f5..615787200d6a 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -17,18 +17,23 @@ //! # 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. +//! This pallet implements [`sp_staking::DelegatedStakeInterface`] that extends [`StakingInterface`] +//! to support delegation of stake. It consumes [`Config::CoreStaking`] to provide primitive staking +//! functions and only implements the delegation features. //! -//! 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. +//! 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 +//! multi location such as a parachain or a smart contract. //! -//! 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. +//! ## 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. +//! - **DelegateeLedger**: 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 //! @@ -37,78 +42,64 @@ //! 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. +//! 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. //! //! #### 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. +//! 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, 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 +//! 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 //! -//! ## 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. +//! - 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. -//! - `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. +//! - 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::claim_delegation`]. //! //! #### [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. +//! [Config::CoreStaking] to provide delegation based staking. Concretely, a pallet like +//! `NominationPools` can switch to this pallet as its Staking provider to support delegation based +//! staking from pool accounts, allowing its members to lock funds in their own account. //! //! ## 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 +//! This pallet implements a lazy slashing mechanism. Any slashes to the `agent` 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 +//! `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 pending -//! slashes are applied. +//! 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 -//! [StakingInterface::delegator_slash](sp_staking::StakingInterface::delegator_slash). +//! [DelegatedStakeInterface::delegator_slash](sp_staking::DelegatedStakeInterface::delegator_slash). //! -//! ## Migration from Nominator to Delegatee +//! ## Migration from Nominator to Agent //! 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 +//! thought of as layer in between 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. +//! 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 @@ -117,7 +108,7 @@ //! //! #### 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. +//! 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 @@ -213,7 +204,7 @@ pub mod pallet { NotAllowed, /// An existing staker cannot perform this action. AlreadyStaking, - /// Reward Destination cannot be `delegatee` account. + /// Reward Destination cannot be same as `Agent` account. InvalidRewardDestination, /// Delegation conditions are not met. /// @@ -223,13 +214,13 @@ pub mod pallet { InvalidDelegation, /// The account does not have enough funds to perform the operation. NotEnoughFunds, - /// Not an existing delegatee account. + /// Not an existing `Agent` account. NotAgent, /// Not a Delegator account. NotDelegator, /// Some corruption in internal state. BadState, - /// Unapplied pending slash restricts operation on `delegatee`. + /// Unapplied pending slash restricts operation on `Agent`. UnappliedSlash, /// Failed to withdraw amount from Core Staking. WithdrawFailed, @@ -249,22 +240,22 @@ pub mod pallet { #[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 }, + Delegated { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf }, /// Funds released to a delegator. - Released { delegatee: T::AccountId, delegator: T::AccountId, amount: BalanceOf }, + Released { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf }, /// Funds slashed from a delegator. - Slashed { delegatee: T::AccountId, delegator: T::AccountId, amount: BalanceOf }, + 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 `delegatee` account + /// 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 `Delegatee` to their `DelegateeLedger`. + /// Map of `Agent` to their `Ledger`. #[pallet::storage] pub(crate) type Delegatees = CountedStorageMap<_, Twox64Concat, T::AccountId, DelegateeLedger, OptionQuery>; @@ -317,7 +308,7 @@ pub mod pallet { /// 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 + /// accounts of the origin can later migrate their funds using [Self::claim_delegation] to /// claim back their share of delegated funds from `proxy_delegator` to self. pub fn migrate_to_agent( origin: OriginFor, @@ -556,7 +547,7 @@ impl Pallet { T::Currency::hold(&HoldReason::Delegating.into(), delegator, amount)?; Self::deposit_event(Event::::Delegated { - delegatee: delegatee.clone(), + agent: delegatee.clone(), delegator: delegator.clone(), amount, }); @@ -608,7 +599,7 @@ impl Pallet { defensive_assert!(released == amount, "hold should have been released fully"); Self::deposit_event(Event::::Released { - delegatee: who.clone(), + agent: who.clone(), delegator: delegator.clone(), amount, }); @@ -735,7 +726,7 @@ impl Pallet { T::OnSlash::on_unbalanced(credit); - Self::deposit_event(Event::::Slashed { delegatee: delegatee_acc, delegator, amount }); + Self::deposit_event(Event::::Slashed { agent: delegatee_acc, delegator, amount }); Ok(()) } From f82b349de2ed919c20831936001905f47398b0fd Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 19:27:28 +0200 Subject: [PATCH 15/85] rename storage Delegatees to Agents --- substrate/frame/delegated-staking/src/impls.rs | 4 ++-- substrate/frame/delegated-staking/src/lib.rs | 6 +++--- substrate/frame/delegated-staking/src/types.rs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index e09a4b373834..7be0866f8ded 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -109,7 +109,7 @@ impl StakingInterface for Pallet { } fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { - let ledger = >::get(who).ok_or(Error::::NotAgent)?; + let ledger = >::get(who).ok_or(Error::::NotAgent)?; ensure!(ledger.stakeable_balance() >= extra, Error::::NotEnoughFunds); T::CoreStaking::bond_extra(who, extra) @@ -277,7 +277,7 @@ impl OnStakingUpdate> for Pallet { _slashed_unlocking: &sp_std::collections::btree_map::BTreeMap>, slashed_total: BalanceOf, ) { - >::mutate(who, |maybe_register| match maybe_register { + >::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 => { diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 615787200d6a..e831c0f95400 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -257,7 +257,7 @@ pub mod pallet { /// Map of `Agent` to their `Ledger`. #[pallet::storage] - pub(crate) type Delegatees = + pub(crate) type Agents = CountedStorageMap<_, Twox64Concat, T::AccountId, DelegateeLedger, OptionQuery>; // This pallet is not currently written with the intention of exposing any calls. But the @@ -448,7 +448,7 @@ impl Pallet { /// Returns true if who is registered as a `Delegatee`. fn is_agent(who: &T::AccountId) -> bool { - >::contains_key(who) + >::contains_key(who) } /// Returns true if who is delegating to a `Delegatee` account. @@ -748,7 +748,7 @@ 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::>(); + let ledger_map = Agents::::iter().collect::>(); Self::check_delegates(ledger_map.clone())?; Self::check_delegators(delegation_map, ledger_map)?; diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 494a616e76f8..c15ed9f71385 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -60,7 +60,7 @@ impl Delegation { .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), + !>::contains_key(delegator), ) } @@ -128,12 +128,12 @@ impl DelegateeLedger { /// Get `DelegateeLedger` from storage. pub(crate) fn get(key: &T::AccountId) -> Option { - >::get(key) + >::get(key) } /// Save self to storage with the given key. pub(crate) fn save(self, key: &T::AccountId) { - >::insert(key, self) + >::insert(key, self) } /// Effective total balance of the `delegatee`. @@ -280,7 +280,7 @@ impl Delegatee { self.ledger.pending_slash == Zero::zero(), Error::::BadState ); - >::remove(key); + >::remove(key); } else { self.ledger.save(&key) } From b5500310206c37304744d5e0b76bb09096e61ee6 Mon Sep 17 00:00:00 2001 From: Ankan Date: Sun, 31 Mar 2024 19:34:23 +0200 Subject: [PATCH 16/85] rename all delegatee to agent --- .../frame/delegated-staking/src/impls.rs | 75 +++--- substrate/frame/delegated-staking/src/lib.rs | 139 ++++++----- substrate/frame/delegated-staking/src/mock.rs | 23 +- .../frame/delegated-staking/src/tests.rs | 222 ++++++++++-------- .../frame/delegated-staking/src/types.rs | 102 ++++---- substrate/primitives/staking/src/lib.rs | 64 +++++ 6 files changed, 340 insertions(+), 285 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 7be0866f8ded..72884718dfde 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -22,10 +22,7 @@ use super::*; use sp_staking::{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`. +/// Wrapper `StakingInterface` implementation for `Agents`. impl StakingInterface for Pallet { type Balance = BalanceOf; type AccountId = T::AccountId; @@ -90,10 +87,10 @@ impl StakingInterface for Pallet { ) -> DispatchResult { // ensure who is not already staked ensure!(T::CoreStaking::status(who).is_err(), Error::::AlreadyStaking); - let delegatee = Delegatee::::from(who)?; + let agent = Agent::::from(who)?; - ensure!(delegatee.available_to_bond() >= value, Error::::NotEnoughFunds); - ensure!(delegatee.ledger.payee == *payee, Error::::InvalidRewardDestination); + ensure!(agent.available_to_bond() >= value, Error::::NotEnoughFunds); + ensure!(agent.ledger.payee == *payee, Error::::InvalidRewardDestination); T::CoreStaking::virtual_bond(who, value, payee) } @@ -116,8 +113,8 @@ impl StakingInterface for Pallet { } fn unbond(stash: &Self::AccountId, value: Self::Balance) -> DispatchResult { - let delegatee = Delegatee::::from(stash)?; - ensure!(delegatee.bonded_stake() >= value, Error::::NotEnoughFunds); + let agent = Agent::::from(stash)?; + ensure!(agent.bonded_stake() >= value, Error::::NotEnoughFunds); T::CoreStaking::unbond(stash, value) } @@ -128,13 +125,13 @@ impl StakingInterface for Pallet { /// Withdraw unbonding funds until current era. /// - /// Funds are moved to unclaimed_withdrawals register of the `DelegateeLedger`. + /// Funds are moved to unclaimed_withdrawals register of the `AgentLedger`. fn withdraw_unbonded( - delegatee_acc: Self::AccountId, + agent_acc: Self::AccountId, num_slashing_spans: u32, ) -> Result { - Pallet::::withdraw_unbonded(&delegatee_acc, num_slashing_spans) - .map(|delegatee| delegatee.ledger.total_delegated.is_zero()) + Pallet::::withdraw_unbonded(&agent_acc, num_slashing_spans) + .map(|agent| agent.ledger.total_delegated.is_zero()) } fn desired_validator_count() -> u32 { @@ -191,10 +188,10 @@ impl StakingInterface for Pallet { } 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()) + /// Effective balance of the `Agent` account. + fn agent_balance(who: &Self::AccountId) -> Self::Balance { + Agent::::from(who) + .map(|agent| agent.ledger.effective_balance()) .unwrap_or_default() } @@ -202,71 +199,63 @@ impl DelegatedStakeInterface for Pallet { Delegation::::get(delegator).map(|d| d.amount).unwrap_or_default() } - /// Delegate funds to `Delegatee`. + /// Delegate funds to an `Agent`. fn delegate( who: &Self::AccountId, - delegatee: &Self::AccountId, + agent: &Self::AccountId, reward_account: &Self::AccountId, amount: Self::Balance, ) -> DispatchResult { Pallet::::register_agent( - RawOrigin::Signed(delegatee.clone()).into(), + RawOrigin::Signed(agent.clone()).into(), reward_account.clone(), )?; - // Delegate the funds from who to the delegatee account. - Pallet::::delegate_to_agent( - RawOrigin::Signed(who.clone()).into(), - delegatee.clone(), - amount, - ) + // 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 delegatee account. + /// Add more delegation to the `Agent` account. fn delegate_extra( who: &Self::AccountId, - delegatee: &Self::AccountId, + agent: &Self::AccountId, amount: Self::Balance, ) -> DispatchResult { - Pallet::::delegate_to_agent( - RawOrigin::Signed(who.clone()).into(), - delegatee.clone(), - amount, - ) + Pallet::::delegate_to_agent(RawOrigin::Signed(who.clone()).into(), agent.clone(), amount) } - /// Withdraw delegation of `delegator` to `delegatee`. + /// Withdraw delegation of `delegator` to `Agent`. /// - /// If there are funds in `delegatee` account that can be withdrawn, then those funds would be + /// 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, - delegatee: &Self::AccountId, + agent: &Self::AccountId, amount: Self::Balance, ) -> DispatchResult { // fixme(ank4n): Can this not require slashing spans? Pallet::::release_delegation( - RawOrigin::Signed(delegatee.clone()).into(), + RawOrigin::Signed(agent.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) + /// Returns true if the `Agent` have any slash pending to be applied. + fn has_pending_slash(agent: &Self::AccountId) -> bool { + Agent::::from(agent) .map(|d| !d.ledger.pending_slash.is_zero()) .unwrap_or(false) } fn delegator_slash( - delegatee: &Self::AccountId, + agent: &Self::AccountId, delegator: &Self::AccountId, value: Self::Balance, maybe_reporter: Option, ) -> sp_runtime::DispatchResult { - Pallet::::do_slash(delegatee.clone(), delegator.clone(), value, maybe_reporter) + Pallet::::do_slash(agent.clone(), delegator.clone(), value, maybe_reporter) } } @@ -278,7 +267,7 @@ impl OnStakingUpdate> for Pallet { slashed_total: BalanceOf, ) { >::mutate(who, |maybe_register| match maybe_register { - // if delegatee, register the slashed amount as pending slash. + // if existing agent, 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 index e831c0f95400..2de5ae44b85b 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -30,8 +30,8 @@ //! 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. -//! - **DelegateeLedger**: A data structure that holds important information about the `agent` such -//! as total delegations they have received, any slashes posted to them, etc. +//! - **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`. //! @@ -79,7 +79,7 @@ //! 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 -//! `DelegateeLedger` as a pending slash. Since the actual amount is held in the multiple +//! `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 @@ -258,11 +258,11 @@ pub mod pallet { /// Map of `Agent` to their `Ledger`. #[pallet::storage] pub(crate) type Agents = - CountedStorageMap<_, Twox64Concat, T::AccountId, DelegateeLedger, OptionQuery>; + 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. + /// 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`. /// @@ -301,7 +301,7 @@ pub mod pallet { /// /// The origin needs to /// - be a `Nominator` with `CoreStaking`, - /// - not already a `Delegatee`, + /// - not already an `Agent`, /// - have enough funds to transfer existential deposit to a delegator account created for /// the migration. /// @@ -367,7 +367,7 @@ pub mod pallet { ensure!(!Self::is_delegator(&delegator), Error::::NotAllowed); ensure!(Self::not_direct_staker(&delegator), Error::::AlreadyStaking); - // ensure delegatee is sane. + // ensure agent is sane. ensure!(Self::is_agent(&agent), Error::::NotAgent); // and has enough delegated balance to migrate. @@ -433,12 +433,9 @@ pub mod pallet { } 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())) + /// 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())) } /// Balance of a delegator that is delegated. @@ -446,12 +443,12 @@ impl Pallet { T::Currency::balance_on_hold(&HoldReason::Delegating.into(), who) } - /// Returns true if who is registered as a `Delegatee`. + /// 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 a `Delegatee` account. + /// Returns true if who is delegating to an `Agent` account. fn is_delegator(who: &T::AccountId) -> bool { >::contains_key(who) } @@ -469,10 +466,11 @@ impl Pallet { } fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) { - DelegateeLedger::::new(reward_account).save(who); + AgentLedger::::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 + // 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); } @@ -508,29 +506,29 @@ impl Pallet { Self::do_delegate(&proxy_delegator, who, stake.total) } - fn do_bond(delegatee_acc: &T::AccountId, amount: BalanceOf) -> DispatchResult { - let delegatee = Delegatee::::from(delegatee_acc)?; + fn do_bond(agent_acc: &T::AccountId, amount: BalanceOf) -> DispatchResult { + let agent = Agent::::from(agent_acc)?; - let available_to_bond = delegatee.available_to_bond(); + let available_to_bond = agent.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) + if agent.is_bonded() { + T::CoreStaking::bond_extra(&agent.key, amount) } else { - T::CoreStaking::virtual_bond(&delegatee.key, amount, delegatee.reward_account()) + T::CoreStaking::virtual_bond(&agent.key, amount, agent.reward_account()) } } fn do_delegate( delegator: &T::AccountId, - delegatee: &T::AccountId, + agent: &T::AccountId, amount: BalanceOf, ) -> DispatchResult { - let mut ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotAgent)?; + let mut ledger = AgentLedger::::get(agent).ok_or(Error::::NotAgent)?; let new_delegation_amount = if let Some(existing_delegation) = Delegation::::get(delegator) { - ensure!(&existing_delegation.delegatee == delegatee, Error::::InvalidDelegation); + ensure!(&existing_delegation.agent == agent, Error::::InvalidDelegation); existing_delegation .amount .checked_add(&amount) @@ -539,15 +537,15 @@ impl Pallet { amount }; - Delegation::::from(delegatee, new_delegation_amount).save_or_kill(delegator); + Delegation::::from(agent, new_delegation_amount).save_or_kill(delegator); ledger.total_delegated = ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - ledger.save(delegatee); + ledger.save(agent); T::Currency::hold(&HoldReason::Delegating.into(), delegator, amount)?; Self::deposit_event(Event::::Delegated { - agent: delegatee.clone(), + agent: agent.clone(), delegator: delegator.clone(), amount, }); @@ -561,24 +559,24 @@ impl Pallet { amount: BalanceOf, num_slashing_spans: u32, ) -> DispatchResult { - let mut delegatee = Delegatee::::from(who)?; + let mut agent = Agent::::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::::NotAgent); + 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. - if delegatee.ledger.unclaimed_withdrawals < amount { - // get the updated delegatee - delegatee = Self::withdraw_unbonded(who, num_slashing_spans)?; + if agent.ledger.unclaimed_withdrawals < amount { + // get the updated agent. + agent = 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); + ensure!(agent.ledger.unclaimed_withdrawals >= amount, Error::::NotEnoughFunds); - // claim withdraw from delegatee. - delegatee.remove_unclaimed_withdraw(amount)?.save_or_kill()?; + // claim withdraw from agent. + agent.remove_unclaimed_withdraw(amount)?.save_or_kill()?; // book keep delegation delegation.amount = delegation @@ -608,17 +606,17 @@ impl Pallet { } fn withdraw_unbonded( - delegatee_acc: &T::AccountId, + agent_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; + ) -> Result, DispatchError> { + let agent = Agent::::from(agent_acc)?; + let pre_total = T::CoreStaking::stake(agent_acc).defensive()?.total; let stash_killed: bool = - T::CoreStaking::withdraw_unbonded(delegatee_acc.clone(), num_slashing_spans) + T::CoreStaking::withdraw_unbonded(agent_acc.clone(), num_slashing_spans) .map_err(|_| Error::::WithdrawFailed)?; - let maybe_post_total = T::CoreStaking::stake(delegatee_acc); + let maybe_post_total = T::CoreStaking::stake(agent_acc); // One of them should be true defensive_assert!( !(stash_killed && maybe_post_total.is_ok()), @@ -630,11 +628,11 @@ impl Pallet { let new_withdrawn = pre_total.checked_sub(&post_total).defensive_ok_or(Error::::BadState)?; - let delegatee = delegatee.add_unclaimed_withdraw(new_withdrawn)?; + let agent = agent.add_unclaimed_withdraw(new_withdrawn)?; - delegatee.clone().save(); + agent.clone().save(); - Ok(delegatee) + Ok(agent) } /// Migrates delegation of `amount` from `source` account to `destination` account. @@ -653,8 +651,7 @@ impl Pallet { ); // update delegations - Delegation::::from(&source_delegation.delegatee, amount) - .save_or_kill(destination_delegator); + Delegation::::from(&source_delegation.agent, amount).save_or_kill(destination_delegator); source_delegation .decrease_delegation(amount) @@ -688,15 +685,15 @@ impl Pallet { } pub fn do_slash( - delegatee_acc: T::AccountId, + agent_acc: T::AccountId, delegator: T::AccountId, amount: BalanceOf, maybe_reporter: Option, ) -> DispatchResult { - let delegatee = Delegatee::::from(&delegatee_acc)?; + let agent = Agent::::from(&agent_acc)?; let delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?; - ensure!(delegation.delegatee == delegatee_acc, Error::::NotAgent); + ensure!(delegation.agent == agent_acc, Error::::NotAgent); ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); let (mut credit, missing) = @@ -706,8 +703,8 @@ impl Pallet { let actual_slash = credit.peek(); - // remove the applied slashed amount from delegatee. - delegatee.remove_slash(actual_slash).save(); + // remove the applied slashed amount from agent. + agent.remove_slash(actual_slash).save(); delegation .decrease_delegation(actual_slash) @@ -726,7 +723,7 @@ impl Pallet { T::OnSlash::on_unbalanced(credit); - Self::deposit_event(Event::::Slashed { agent: delegatee_acc, delegator, amount }); + Self::deposit_event(Event::::Slashed { agent: agent_acc, delegator, amount }); Ok(()) } @@ -734,8 +731,8 @@ impl Pallet { /// 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()) + Agent::::from(who) + .map(|agent| agent.ledger.stakeable_balance()) .unwrap_or_default() } } @@ -757,21 +754,21 @@ impl Pallet { } fn check_delegates( - ledgers: BTreeMap>, + ledgers: BTreeMap>, ) -> Result<(), sp_runtime::TryRuntimeError> { - for (delegatee, ledger) in ledgers { + for (agent, ledger) in ledgers { ensure!( matches!( - T::CoreStaking::status(&delegatee).expect("delegatee should be bonded"), + T::CoreStaking::status(&agent).expect("agent should be bonded"), StakerStatus::Nominator(_) | StakerStatus::Idle ), - "delegatee should be bonded and not validator" + "agent should be bonded and not validator" ); ensure!( ledger.stakeable_balance() >= - T::CoreStaking::total_stake(&delegatee) - .expect("delegatee should exist as a nominator"), + T::CoreStaking::total_stake(&agent) + .expect("agent should exist as a nominator"), "Cannot stake more than balance" ); } @@ -781,7 +778,7 @@ impl Pallet { fn check_delegators( delegations: BTreeMap>, - ledger: BTreeMap>, + ledger: BTreeMap>, ) -> Result<(), sp_runtime::TryRuntimeError> { let mut delegation_aggregation = BTreeMap::>::new(); for (delegator, delegation) in delegations.iter() { @@ -789,18 +786,18 @@ impl Pallet { T::CoreStaking::status(delegator).is_err(), "delegator should not be directly staked" ); - ensure!(!Self::is_agent(delegator), "delegator cannot be delegatee"); + ensure!(!Self::is_agent(delegator), "delegator cannot be an agent"); delegation_aggregation - .entry(delegation.delegatee.clone()) + .entry(delegation.agent.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"); + for (agent, total_delegated) in delegation_aggregation { + ensure!(!Self::is_delegator(&agent), "agent cannot be delegator"); - let ledger = ledger.get(&delegatee).expect("ledger should exist"); + let ledger = ledger.get(&agent).expect("ledger should exist"); ensure!( ledger.total_delegated == total_delegated, "ledger total delegated should match delegations" diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 1a4f079735b7..f98e0c5843bb 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{self as delegated_staking, types::Delegatee, HoldReason}; +use crate::{self as delegated_staking, types::Agent, HoldReason}; use frame_support::{ assert_ok, derive_impl, pallet_prelude::*, @@ -257,17 +257,14 @@ pub(crate) fn fund(who: &AccountId, amount: Balance) { /// `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, + agent: AccountId, reward_acc: AccountId, delegators: Vec, base_delegate_amount: Balance, increment: Balance, ) -> Balance { - fund(&delegatee, 100); - assert_ok!(DelegatedStaking::register_agent( - RawOrigin::Signed(delegatee).into(), - reward_acc - )); + 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; @@ -276,14 +273,14 @@ pub(crate) fn setup_delegation_stake( fund(delegator, amount_to_delegate + ExistentialDeposit::get()); assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(*delegator).into(), - delegatee, + agent, amount_to_delegate )); } // sanity checks - assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), delegated_amount); - assert_eq!(Delegatee::::from(&delegatee).unwrap().available_to_bond(), 0); + assert_eq!(DelegatedStaking::stakeable_balance(&agent), delegated_amount); + assert_eq!(Agent::::from(&agent).unwrap().available_to_bond(), 0); delegated_amount } @@ -294,11 +291,11 @@ pub(crate) fn start_era(era: sp_staking::EraIndex) { 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 + get_agent(&who).ledger.stakeable_balance() == total } -pub(crate) fn get_delegatee(delegatee: &AccountId) -> Delegatee { - Delegatee::::from(delegatee).expect("delegate should exist") +pub(crate) fn get_agent(agent: &AccountId) -> Agent { + Agent::::from(agent).expect("delegate should exist") } #[allow(unused)] diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 9b9dd92bc227..715436153081 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -23,16 +23,16 @@ use frame_support::{assert_noop, assert_ok, traits::fungible::InspectHold}; use pallet_staking::Error as StakingError; #[test] -fn create_a_delegatee_with_first_delegator() { +fn create_a_agent_with_first_delegator() { ExtBuilder::default().build_and_execute(|| { - let delegatee: AccountId = 200; + let agent: AccountId = 200; let reward_account: AccountId = 201; let delegator: AccountId = 202; // set intention to accept delegation. - fund(&delegatee, 1000); + fund(&agent, 1000); assert_ok!(DelegatedStaking::register_agent( - RawOrigin::Signed(delegatee).into(), + RawOrigin::Signed(agent).into(), reward_account )); @@ -40,27 +40,27 @@ fn create_a_delegatee_with_first_delegator() { fund(&delegator, 1000); assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator).into(), - delegatee, + agent, 100 )); // verify - assert!(DelegatedStaking::is_agent(&delegatee)); - assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 100); + assert!(DelegatedStaking::is_agent(&agent)); + assert_eq!(DelegatedStaking::stakeable_balance(&agent), 100); assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator), 100); }); } #[test] -fn cannot_become_delegatee() { +fn cannot_become_agent() { ExtBuilder::default().build_and_execute(|| { - // cannot set reward account same as delegatee account + // 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 delegatee + // an existing validator cannot become agent assert_noop!( DelegatedStaking::register_agent( RawOrigin::Signed(mock::GENESIS_VALIDATOR).into(), @@ -69,7 +69,7 @@ fn cannot_become_delegatee() { Error::::AlreadyStaking ); - // an existing nominator cannot become delegatee + // an existing nominator cannot become agent assert_noop!( DelegatedStaking::register_agent( RawOrigin::Signed(mock::GENESIS_NOMINATOR_ONE).into(), @@ -90,17 +90,17 @@ fn cannot_become_delegatee() { #[test] fn create_multiple_delegators() { ExtBuilder::default().build_and_execute(|| { - let delegatee: AccountId = 200; + let agent: AccountId = 200; let reward_account: AccountId = 201; - // stakeable balance is 0 for non delegatee - fund(&delegatee, 1000); - assert!(!DelegatedStaking::is_agent(&delegatee)); - assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 0); + // 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(delegatee).into(), + RawOrigin::Signed(agent).into(), reward_account )); @@ -109,65 +109,61 @@ fn create_multiple_delegators() { fund(&i, 100 + ExistentialDeposit::get()); assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(i).into(), - delegatee, + agent, 100 )); - // Balance of 100 held on delegator account for delegating to the delegatee. + // Balance of 100 held on delegator account for delegating to the agent. assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &i), 100); } // verify - assert!(DelegatedStaking::is_agent(&delegatee)); - assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 100 * 100); + assert!(DelegatedStaking::is_agent(&agent)); + assert_eq!(DelegatedStaking::stakeable_balance(&agent), 100 * 100); }); } #[test] -fn delegatee_restrictions() { +fn agent_restrictions() { // Similar to creating a nomination pool ExtBuilder::default().build_and_execute(|| { - let delegatee_one = 200; + let agent_one = 200; let delegator_one = 210; - fund(&delegatee_one, 100); + fund(&agent_one, 100); assert_ok!(DelegatedStaking::register_agent( - RawOrigin::Signed(delegatee_one).into(), - delegatee_one + 1 + RawOrigin::Signed(agent_one).into(), + agent_one + 1 )); fund(&delegator_one, 200); assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator_one).into(), - delegatee_one, + agent_one, 100 )); - let delegatee_two = 300; + let agent_two = 300; let delegator_two = 310; - fund(&delegatee_two, 100); + fund(&agent_two, 100); assert_ok!(DelegatedStaking::register_agent( - RawOrigin::Signed(delegatee_two).into(), - delegatee_two + 1 + RawOrigin::Signed(agent_two).into(), + agent_two + 1 )); fund(&delegator_two, 200); assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator_two).into(), - delegatee_two, + agent_two, 100 )); - // delegatee one tries to delegate to delegatee 2 + // agent one tries to delegate to agent 2 assert_noop!( - DelegatedStaking::delegate_to_agent( - RawOrigin::Signed(delegatee_one).into(), - delegatee_two, - 10 - ), + DelegatedStaking::delegate_to_agent(RawOrigin::Signed(agent_one).into(), agent_two, 10), Error::::InvalidDelegation ); - // delegatee one tries to delegate to a delegator + // agent one tries to delegate to a delegator assert_noop!( DelegatedStaking::delegate_to_agent( - RawOrigin::Signed(delegatee_one).into(), + RawOrigin::Signed(agent_one).into(), delegator_one, 10 ), @@ -175,19 +171,19 @@ fn delegatee_restrictions() { ); assert_noop!( DelegatedStaking::delegate_to_agent( - RawOrigin::Signed(delegatee_one).into(), + RawOrigin::Signed(agent_one).into(), delegator_two, 10 ), Error::::InvalidDelegation ); - // delegator one tries to delegate to delegatee 2 as well (it already delegates to delegatee + // 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(), - delegatee_two, + agent_two, 10 ), Error::::InvalidDelegation @@ -211,17 +207,17 @@ mod staking_integration { #[test] fn bond() { ExtBuilder::default().build_and_execute(|| { - let delegatee: AccountId = 99; + let agent: AccountId = 99; let reward_acc: AccountId = 100; - assert_eq!(Staking::status(&delegatee), Err(StakingError::::NotStash.into())); + assert_eq!(Staking::status(&agent), Err(StakingError::::NotStash.into())); - // set intention to become a delegatee - fund(&delegatee, 100); + // set intention to become an agent + fund(&agent, 100); assert_ok!(DelegatedStaking::register_agent( - RawOrigin::Signed(delegatee).into(), + RawOrigin::Signed(agent).into(), reward_acc )); - assert_eq!(DelegatedStaking::stakeable_balance(&delegatee), 0); + assert_eq!(DelegatedStaking::stakeable_balance(&agent), 0); let mut delegated_balance: Balance = 0; @@ -230,7 +226,7 @@ mod staking_integration { fund(&delegator, 200); assert_ok!(DelegatedStaking::delegate_to_agent( RawOrigin::Signed(delegator).into(), - delegatee, + agent, 100 )); delegated_balance += 100; @@ -239,16 +235,13 @@ mod staking_integration { 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); + 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(&delegatee).unwrap(), - Stake { total: 50 * 100, active: 50 * 100 } - ) + assert_eq!(Staking::stake(&agent).unwrap(), Stake { total: 50 * 100, active: 50 * 100 }) }); } @@ -257,89 +250,109 @@ mod staking_integration { ExtBuilder::default().build_and_execute(|| { // initial era start_era(1); - let delegatee: AccountId = 200; + let agent: 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); + setup_delegation_stake(agent, reward_acc, delegators.clone(), 10, 10); // lets go to a new era start_era(2); - assert!(eq_stake(delegatee, total_staked, total_staked)); + assert!(eq_stake(agent, total_staked, total_staked)); // Withdrawing without unbonding would fail. assert_noop!( - DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 301, 50, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 301, 50, 0), Error::::NotEnoughFunds ); - // assert_noop!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 200, 50, - // 0), Error::::NotAllowed); active and total stake remains same - assert!(eq_stake(delegatee, total_staked, total_staked)); + // assert_noop!(DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), + // 200, 50, 0), Error::::NotAllowed); active and total stake remains same + assert!(eq_stake(agent, total_staked, total_staked)); // 305 wants to unbond 50 in era 2, withdrawable in era 5. - assert_ok!(DelegatedStaking::unbond(&delegatee, 50)); + assert_ok!(DelegatedStaking::unbond(&agent, 50)); // 310 wants to unbond 100 in era 3, withdrawable in era 6. start_era(3); - assert_ok!(DelegatedStaking::unbond(&delegatee, 100)); + assert_ok!(DelegatedStaking::unbond(&agent, 100)); // 320 wants to unbond 200 in era 4, withdrawable in era 7. start_era(4); - assert_ok!(DelegatedStaking::unbond(&delegatee, 200)); + assert_ok!(DelegatedStaking::unbond(&agent, 200)); // active stake is now reduced.. let expected_active = total_staked - (50 + 100 + 200); - assert!(eq_stake(delegatee, total_staked, expected_active)); + assert!(eq_stake(agent, total_staked, expected_active)); // nothing to withdraw at era 4 assert_noop!( - DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 305, 50, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 305, 50, 0), Error::::NotEnoughFunds ); - assert!(eq_stake(delegatee, total_staked, expected_active)); - assert_eq!(get_delegatee(&delegatee).available_to_bond(), 0); + assert!(eq_stake(agent, total_staked, expected_active)); + assert_eq!(get_agent(&agent).available_to_bond(), 0); // full amount is still delegated - assert_eq!(get_delegatee(&delegatee).ledger.effective_balance(), total_staked); + 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(delegatee).into(), 305, 51, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 305, 51, 0), Error::::NotEnoughFunds ); // less is possible - assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 305, 30, 0)); - assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 305, 20, 0)); + 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(delegatee).into(), 305, 5, 0), + 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(delegatee).into(), 309, 91, 0), + 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(delegatee).into(), 310, 101, 0), + 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(delegatee).into(), 310, 100, 0)); + 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(delegatee).into(), 320, 200, 0)); + assert_ok!(DelegatedStaking::release_delegation( + RawOrigin::Signed(agent).into(), + 320, + 200, + 0 + )); // cannot withdraw anything more.. assert_noop!( - DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 301, 1, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 301, 1, 0), Error::::NotEnoughFunds ); assert_noop!( - DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 350, 1, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 350, 1, 0), Error::::NotEnoughFunds ); }); @@ -348,12 +361,12 @@ mod staking_integration { #[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); + 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(delegatee).into(), 300, 100, 0), + DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 300, 100, 0), Error::::NotEnoughFunds ); @@ -363,15 +376,15 @@ mod staking_integration { // withdrawn and test its claimed from there first. // fund(&300, 1000); - // assert_ok!(DelegatedStaking::delegate_to_agent(RawOrigin::Signed(300.into()), delegate, - // 100)); + // assert_ok!(DelegatedStaking::delegate_to_agent(RawOrigin::Signed(300.into()), + // delegate, 100)); // // // verify unbonded balance - // assert_eq!(get_delegatee(&delegatee).available_to_bond(), 100); + // assert_eq!(get_agent(&agent).available_to_bond(), 100); // // // withdraw works now without unbonding - // assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(delegatee).into(), 300, 100, - // 0)); assert_eq!(get_delegatee(&delegatee).available_to_bond(), 0); + // assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 300, + // 100, 0)); assert_eq!(get_agent(&agent).available_to_bond(), 0); }); } @@ -382,7 +395,7 @@ mod staking_integration { fund(&200, 1000); let balance_200 = Balances::free_balance(200); - // `delegatee` account cannot be reward destination + // `Agent` account cannot be reward destination assert_noop!( DelegatedStaking::register_agent(RawOrigin::Signed(200).into(), 200), Error::::InvalidRewardDestination @@ -392,7 +405,11 @@ mod staking_integration { 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)); + assert_ok!(DelegatedStaking::delegate_to_agent( + RawOrigin::Signed(300).into(), + 200, + 100 + )); // if delegate calls Staking pallet directly with a different reward destination, it // fails. @@ -409,8 +426,8 @@ mod staking_integration { // 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); + 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); @@ -418,7 +435,7 @@ mod staking_integration { } #[test] - fn delegatee_restrictions() { + fn agent_restrictions() { ExtBuilder::default().build_and_execute(|| { setup_delegation_stake(200, 201, (202..203).collect(), 100, 0); @@ -441,10 +458,7 @@ mod staking_integration { Error::::AlreadyStaking ); assert_noop!( - DelegatedStaking::register_agent( - RawOrigin::Signed(GENESIS_VALIDATOR).into(), - 201 - ), + DelegatedStaking::register_agent(RawOrigin::Signed(GENESIS_VALIDATOR).into(), 201), Error::::AlreadyStaking ); }); @@ -498,8 +512,8 @@ mod staking_integration { 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); + assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); + assert_eq!(get_agent(&200).available_to_bond(), 0); // now lets migrate the delegators let delegator_share = staked_amount / 4; @@ -525,8 +539,8 @@ mod staking_integration { // 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); + assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); + assert_eq!(get_agent(&200).available_to_bond(), 0); } // cannot use migrate delegator anymore diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index c15ed9f71385..70965374305c 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -24,9 +24,9 @@ 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. + /// A proxy delegator account created for a nominator who migrated to an `Agent` account. /// - /// Funds for unmigrated `delegator` accounts of the `delegatee` are kept here. + /// Funds for unmigrated `delegator` accounts of the `Agent` are kept here. ProxyDelegator, } @@ -35,7 +35,7 @@ pub(crate) enum AccountType { #[scale_info(skip_type_params(T))] pub struct Delegation { /// The target of delegation. - pub delegatee: T::AccountId, + pub agent: T::AccountId, /// The amount delegated. pub amount: BalanceOf, } @@ -47,19 +47,19 @@ impl Delegation { } /// Create and return a new delegation instance. - pub(crate) fn from(delegatee: &T::AccountId, amount: BalanceOf) -> Self { - Delegation { delegatee: delegatee.clone(), amount } + pub(crate) fn from(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 delegatee. + /// existing agent. /// - /// Delegators are prevented from delegating to multiple delegatees at the same time. - pub(crate) fn can_delegate(delegator: &T::AccountId, delegatee: &T::AccountId) -> bool { + /// 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.delegatee == delegatee.clone()) + .map(|delegation| delegation.agent == agent.clone()) .unwrap_or( - // all good if its a new delegator except it should not be an existing delegatee. + // all good if it is a new delegator except it should not be an existing agent. !>::contains_key(delegator), ) } @@ -67,14 +67,14 @@ impl Delegation { /// 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)) + Some(Delegation::from(&self.agent, 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)) + Some(Delegation::from(&self.agent, updated_delegation)) } /// Save self to storage. If the delegation amount is zero, remove the delegation. @@ -89,36 +89,36 @@ impl Delegation { } } -/// Ledger of all delegations to a `Delegatee`. +/// Ledger of all delegations to an `Agent`. /// -/// 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 +/// 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 DelegateeLedger { +pub struct AgentLedger { /// Where the reward should be paid out. pub payee: T::AccountId, - /// Sum of all delegated funds to this `delegatee`. + /// 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 `delegatee` does not bond funds from delegated + /// 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. // 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`. + /// Slashes that are not yet applied. This affects the effective balance of the `Agent`. #[codec(compact)] pub pending_slash: BalanceOf, } -impl DelegateeLedger { - /// Create a new instance of `DelegateeLedger`. +impl AgentLedger { + /// Create a new instance of `AgentLedger`. pub(crate) fn new(reward_destination: &T::AccountId) -> Self { - DelegateeLedger { + AgentLedger { payee: reward_destination.clone(), total_delegated: Zero::zero(), unclaimed_withdrawals: Zero::zero(), @@ -126,7 +126,7 @@ impl DelegateeLedger { } } - /// Get `DelegateeLedger` from storage. + /// Get `AgentLedger` from storage. pub(crate) fn get(key: &T::AccountId) -> Option { >::get(key) } @@ -136,9 +136,9 @@ impl DelegateeLedger { >::insert(key, self) } - /// Effective total balance of the `delegatee`. + /// Effective total balance of the `Agent`. /// - /// This takes into account any slashes reported to `Delegatee` but unapplied. + /// 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, @@ -149,26 +149,26 @@ impl DelegateeLedger { self.total_delegated.saturating_sub(self.pending_slash) } - /// Delegatee balance that can be staked/bonded in [`T::CoreStaking`]. + /// 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 `DelegateeLedger` to provide additional functionality. +/// Wrapper around `AgentLedger` to provide some helper functions to mutate the ledger. #[derive(Clone)] -pub struct Delegatee { +pub struct Agent { /// storage key pub key: T::AccountId, /// storage value - pub ledger: DelegateeLedger, + pub ledger: AgentLedger, } -impl Delegatee { - /// Get `Delegatee` from storage if it exists or return an error. - pub(crate) fn from(delegatee: &T::AccountId) -> Result, DispatchError> { - let ledger = DelegateeLedger::::get(delegatee).ok_or(Error::::NotAgent)?; - Ok(Delegatee { key: delegatee.clone(), ledger }) +impl Agent { + /// Get `Agent` from storage if it exists or return an error. + pub(crate) fn from(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. @@ -190,8 +190,8 @@ impl Delegatee { .checked_sub(&amount) .defensive_ok_or(ArithmeticError::Overflow)?; - Ok(Delegatee { - ledger: DelegateeLedger { + Ok(Agent { + ledger: AgentLedger { total_delegated: new_total_delegated, unclaimed_withdrawals: new_unclaimed_withdrawals, ..self.ledger @@ -211,11 +211,8 @@ impl Delegatee { .checked_add(&amount) .defensive_ok_or(ArithmeticError::Overflow)?; - Ok(Delegatee { - ledger: DelegateeLedger { - unclaimed_withdrawals: new_unclaimed_withdrawals, - ..self.ledger - }, + Ok(Agent { + ledger: AgentLedger { unclaimed_withdrawals: new_unclaimed_withdrawals, ..self.ledger }, ..self }) } @@ -230,34 +227,31 @@ impl Delegatee { defensive_assert!( stakeable >= bonded_stake, - "cannot be bonded with more than delegatee balance" + "cannot be bonded with more than total amount delegated to agent" ); stakeable.saturating_sub(bonded_stake) } - /// Remove slashes from the `DelegateeLedger`. + /// 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); - Delegatee { - ledger: DelegateeLedger { pending_slash, total_delegated, ..self.ledger }, - ..self - } + Agent { ledger: AgentLedger { pending_slash, total_delegated, ..self.ledger }, ..self } } - /// Get the total stake of delegatee bonded in [`Config::CoreStaking`]. + /// 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 delegatee is bonded in [`Config::CoreStaking`]. + /// 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 delegatee. + /// Returns the reward account registered by the agent. pub(crate) fn reward_account(&self) -> &T::AccountId { &self.ledger.payee } @@ -291,14 +285,14 @@ impl Delegatee { /// Reloads self from storage. #[cfg(test)] #[allow(unused)] - pub(crate) fn refresh(&self) -> Result, DispatchError> { + pub(crate) fn refresh(&self) -> Result, DispatchError> { Self::from(&self.key) } - /// Balance of `Delegatee` that is not bonded. + /// Balance of `Agent` that is not bonded. /// /// This is similar to [Self::available_to_bond] except it also includes `unclaimed_withdrawals` - /// of `Delegatee`. + /// of `Agent`. #[cfg(test)] #[allow(unused)] pub(crate) fn total_unbonded(&self) -> BalanceOf { @@ -308,7 +302,7 @@ impl Delegatee { defensive_assert!( net_balance >= bonded_stake, - "cannot be bonded with more than the delegatee balance" + "cannot be bonded with more than the agent balance" ); net_balance.saturating_sub(bonded_stake) diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 0e1812e10ab2..13cfbe8e326d 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -449,4 +449,68 @@ pub struct PagedExposureMetadata { pub page_count: Page, } +/// Extension of [`StakingInterface`] with delegation functionality. +/// +/// Introduces two new actors: +/// - `Delegator`: An account that delegates funds to a `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. +/// +/// The `Agent` is responsible for managing rewards and slashing for all the `Delegators` that +/// have delegated funds to it. +pub trait DelegatedStakeInterface: StakingInterface { + /// 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, + ) -> 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 a `Agent` by slashing `value` from `delegator`. + /// + /// If a reporter is provided, the reporter will receive a fraction of the slash as reward. + fn delegator_slash( + agent: &Self::AccountId, + delegator: &Self::AccountId, + value: Self::Balance, + maybe_reporter: Option, + ) -> sp_runtime::DispatchResult; +} + sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); From b87fc08489dad3ee736235af3a894f753fd86957 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 1 Apr 2024 12:01:51 +0200 Subject: [PATCH 17/85] remove fixme --- substrate/frame/delegated-staking/src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 70965374305c..d48579226574 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -107,7 +107,6 @@ pub struct AgentLedger { /// /// 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. - // 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 `Agent`. From bfd34343ebd24f061a055049bb182ad316e2a882 Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 2 Apr 2024 11:50:10 +0200 Subject: [PATCH 18/85] Make delegation interface independent --- .../frame/delegated-staking/src/impls.rs | 179 ++---------------- substrate/frame/delegated-staking/src/lib.rs | 8 +- .../frame/delegated-staking/src/tests.rs | 10 +- substrate/primitives/staking/src/lib.rs | 27 ++- 4 files changed, 49 insertions(+), 175 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 72884718dfde..56ad399ef494 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -16,178 +16,16 @@ // 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 +//! Implementations of public traits, namely [DelegationInterface] and //! [OnStakingUpdate]. use super::*; -use sp_staking::{DelegatedStakeInterface, OnStakingUpdate}; +use sp_staking::{DelegationInterface, OnStakingUpdate}; -/// Wrapper `StakingInterface` implementation for `Agents`. -impl StakingInterface for Pallet { +impl DelegationInterface 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_agent(who), Error::::NotSupported); - T::CoreStaking::stake(who) - } - - fn total_stake(who: &Self::AccountId) -> Result { - if Self::is_agent(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_agent(who), Error::::NotSupported); - 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 agent = Agent::::from(who)?; - - ensure!(agent.available_to_bond() >= value, Error::::NotEnoughFunds); - ensure!(agent.ledger.payee == *payee, Error::::InvalidRewardDestination); - - T::CoreStaking::virtual_bond(who, value, payee) - } - - fn nominate(who: &Self::AccountId, validators: Vec) -> DispatchResult { - ensure!(Self::is_agent(who), Error::::NotAgent); - T::CoreStaking::nominate(who, validators) - } - - fn chill(who: &Self::AccountId) -> DispatchResult { - ensure!(Self::is_agent(who), Error::::NotAgent); - T::CoreStaking::chill(who) - } - - fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { - let ledger = >::get(who).ok_or(Error::::NotAgent)?; - ensure!(ledger.stakeable_balance() >= extra, Error::::NotEnoughFunds); - - T::CoreStaking::bond_extra(who, extra) - } - - fn unbond(stash: &Self::AccountId, value: Self::Balance) -> DispatchResult { - let agent = Agent::::from(stash)?; - ensure!(agent.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 `AgentLedger`. - fn withdraw_unbonded( - agent_acc: Self::AccountId, - num_slashing_spans: u32, - ) -> Result { - Pallet::::withdraw_unbonded(&agent_acc, num_slashing_spans) - .map(|agent| agent.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_agent(who), Error::::NotAgent); - 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 `Agent` account. fn agent_balance(who: &Self::AccountId) -> Self::Balance { Agent::::from(who) @@ -242,6 +80,17 @@ impl DelegatedStakeInterface for Pallet { ) } + /// Withdraw unbonding funds until current era. + /// + /// Funds are moved to unclaimed_withdrawals register of the `AgentLedger`. + fn withdraw_unclaimed( + agent_acc: Self::AccountId, + num_slashing_spans: u32, + ) -> Result { + Pallet::::withdraw_unbonded(&agent_acc, num_slashing_spans) + .map(|agent| agent.ledger.total_delegated.is_zero()) + } + /// Returns true if the `Agent` have any slash pending to be applied. fn has_pending_slash(agent: &Self::AccountId) -> bool { Agent::::from(agent) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 2de5ae44b85b..89a6a581ca35 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -17,7 +17,7 @@ //! # Delegated Staking Pallet //! -//! This pallet implements [`sp_staking::DelegatedStakeInterface`] that extends [`StakingInterface`] +//! This pallet implements [`sp_staking::DelegationInterface`] that extends [`StakingInterface`] //! to support delegation of stake. It consumes [`Config::CoreStaking`] to provide primitive staking //! functions and only implements the delegation features. //! @@ -86,7 +86,7 @@ //! slashes are cleared. //! //! The user of this pallet can apply slash using -//! [DelegatedStakeInterface::delegator_slash](sp_staking::DelegatedStakeInterface::delegator_slash). +//! [DelegationInterface::delegator_slash](sp_staking::DelegationInterface::delegator_slash). //! //! ## Migration from Nominator to Agent //! More details [here](https://hackmd.io/@ak0n/np-delegated-staking-migration). @@ -157,9 +157,9 @@ use frame_support::{ use sp_runtime::{ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero}, - ArithmeticError, DispatchResult, Perbill, RuntimeDebug, Saturating, + ArithmeticError, DispatchResult, RuntimeDebug, Saturating, }; -use sp_staking::{EraIndex, Stake, StakerStatus, StakingInterface, StakingUnsafe}; +use sp_staking::{EraIndex, StakerStatus, StakingInterface, StakingUnsafe}; use sp_std::{convert::TryInto, prelude::*}; pub type BalanceOf = diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 715436153081..3abc9e4a6d67 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -270,13 +270,13 @@ mod staking_integration { assert!(eq_stake(agent, total_staked, total_staked)); // 305 wants to unbond 50 in era 2, withdrawable in era 5. - assert_ok!(DelegatedStaking::unbond(&agent, 50)); + 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!(DelegatedStaking::unbond(&agent, 100)); + 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!(DelegatedStaking::unbond(&agent, 200)); + assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 200)); // active stake is now reduced.. let expected_active = total_staked - (50 + 100 + 200); @@ -511,7 +511,7 @@ mod staking_integration { Balances::free_balance(200), 5000 - staked_amount - ExistentialDeposit::get() ); - assert_eq!(DelegatedStaking::stake(&200).unwrap(), init_stake); + assert_eq!(Staking::stake(&200).unwrap(), init_stake); assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); assert_eq!(get_agent(&200).available_to_bond(), 0); @@ -538,7 +538,7 @@ mod staking_integration { ); // delegate stake is unchanged. - assert_eq!(DelegatedStaking::stake(&200).unwrap(), init_stake); + assert_eq!(Staking::stake(&200).unwrap(), init_stake); assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); assert_eq!(get_agent(&200).available_to_bond(), 0); } diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 13cfbe8e326d..eed739c0df59 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -458,7 +458,21 @@ pub struct PagedExposureMetadata { /// /// The `Agent` is responsible for managing rewards and slashing for all the `Delegators` that /// have delegated funds to it. -pub trait DelegatedStakeInterface: StakingInterface { +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`. @@ -496,6 +510,17 @@ pub trait DelegatedStakeInterface: StakingInterface { amount: Self::Balance, ) -> DispatchResult; + /// Withdraw any unlocking funds in `CoreStaking` that can be claimed later by a delegator. + /// + /// `CoreStaking` has a limitation on maximum unlocking chunks at any given time. If the limit + /// is reached, we want to implicitly unlock these funds even though a delegator is not + /// present to claim it. Not doing this would block any unbonding until unlocking funds are + /// claimed. + fn withdraw_unclaimed( + agent: Self::AccountId, + num_slashing_spans: u32, + ) -> Result; + /// 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` From 7104fafa86a541d09cb4bae21b0531abfef4cc84 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 07:01:43 +0200 Subject: [PATCH 19/85] move reward account check to caller --- substrate/frame/staking/src/ledger.rs | 10 -------- substrate/frame/staking/src/pallet/impls.rs | 26 +++++++-------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs index 4d3bfe980482..01531703c316 100644 --- a/substrate/frame/staking/src/ledger.rs +++ b/substrate/frame/staking/src/ledger.rs @@ -210,11 +210,6 @@ impl StakingLedger { return Err(Error::::AlreadyBonded) } - // check if the payee is ok. - if Pallet::::restrict_reward_destination(&self.stash, payee.clone()) { - return Err(Error::::RewardDestinationRestricted); - } - >::insert(&self.stash, payee); >::insert(&self.stash, &self.stash); self.update() @@ -226,11 +221,6 @@ impl StakingLedger { return Err(Error::::NotStash) } - // check if the payee is ok. - if Pallet::::restrict_reward_destination(&self.stash, payee.clone()) { - return Err(Error::::RewardDestinationRestricted); - } - >::insert(&self.stash, payee); Ok(()) } diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 5fda07fecf4a..bd27a328aa21 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1168,24 +1168,6 @@ impl Pallet { EraInfo::::get_full_exposure(era, account) } - /// Whether the passed reward destination is restricted for the given account. - /// - /// virtual stakers are not allowed to compound their rewards as this pallet does not manage - /// locks for them. For external pallets that manage the virtual bond, it is their - /// responsibility to distribute the reward and re-bond them. - /// - /// Conservatively, we expect them to always set the reward destination to a non stash account. - pub(crate) fn restrict_reward_destination( - who: &T::AccountId, - reward_destination: RewardDestination, - ) -> bool { - Self::is_virtual_staker(who) && - match reward_destination { - RewardDestination::Account(payee) => payee == *who, - _ => true, - } - } - /// Whether `who` is a virtual staker whose funds are managed by another pallet. pub(crate) fn is_virtual_staker(who: &T::AccountId) -> bool { VirtualStakers::::contains_key(who) @@ -1827,6 +1809,14 @@ impl StakingInterface for Pallet { } fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult { + // Since virtual stakers are not allowed to compound their rewards as this pallet does not + // manage their locks, we do not allow reward account to be set same as stash. For + // external pallets that manage the virtual bond, they can claim rewards and re-bond them. + ensure!( + !Self::is_virtual_staker(stash) || stash != reward_acc, + Error::::RewardDestinationRestricted + ); + // since controller is deprecated and this function is never used for old ledgers with // distinct controllers, we can safely assume that stash is the controller. Self::set_payee( From aed693c14b97d4c6f40a5d45306ed22f612f18e6 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 07:11:53 +0200 Subject: [PATCH 20/85] virtual staker cannot bond again --- substrate/frame/staking/src/tests.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index bde67760cb0e..094de96f19a5 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6943,6 +6943,32 @@ mod staking_unsafe { ); }); } + + #[test] + fn virtual_staker_cannot_bond_again() { + ExtBuilder::default().build_and_execute(|| { + // 10 virtual bonds + assert_ok!(::virtual_bond(&10, 100, &11)); + + // Tries bonding again + assert_noop!( + ::virtual_bond(&10, 20, &11), + Error::::AlreadyBonded + ); + + // And again with a different reward destination. + assert_noop!( + ::virtual_bond(&10, 20, &12), + Error::::AlreadyBonded + ); + + // Direct bond is not allowed as well. + assert_noop!( + ::bond(&10, 20, &12), + Error::::AlreadyBonded + ); + }); + } } mod ledger { From 989bc500b4ba6a6174441c02ec462d7142232a28 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 07:19:46 +0200 Subject: [PATCH 21/85] release all becomes migrate_to_virtual_staker --- substrate/frame/staking/src/pallet/impls.rs | 5 +++-- substrate/frame/staking/src/tests.rs | 5 +++++ substrate/primitives/staking/src/lib.rs | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index bd27a328aa21..029146935861 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1942,8 +1942,9 @@ impl StakingInterface for Pallet { } impl sp_staking::StakingUnsafe for Pallet { - fn force_release(who: &Self::AccountId) { - T::Currency::remove_lock(crate::STAKING_ID, who) + fn migrate_to_virtual_staker(who: &Self::AccountId) { + T::Currency::remove_lock(crate::STAKING_ID, who); + VirtualStakers::::insert(who, ()); } fn virtual_bond( diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 094de96f19a5..67cc5b54b1a8 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6969,6 +6969,11 @@ mod staking_unsafe { ); }); } + + #[test] + fn migrate_virtual_staker(){ + // TODO(ank4n) test migration integrity + } } mod ledger { diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 0e1812e10ab2..fec915587d40 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -317,8 +317,8 @@ pub trait StakingInterface { pub trait StakingUnsafe: StakingInterface { /// Release all funds bonded for stake without unbonding the ledger. /// - /// Unsafe, only used for migration of `nominator` to `virtual_nominator`. - fn force_release(who: &Self::AccountId); + /// Unsafe, should only used for migration of `nominator` to `virtual_nominator`. + fn migrate_to_virtual_staker(who: &Self::AccountId); /// Book-keep a new bond for `who` without applying any locks (hence virtual). /// From 4226b9b4a124f8e2dfb64ada2c98f9ceabc2d155 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 07:37:37 +0200 Subject: [PATCH 22/85] fix function call --- substrate/frame/delegated-staking/src/lib.rs | 2 +- substrate/frame/delegated-staking/src/tests.rs | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 89a6a581ca35..10992a0177ca 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -493,7 +493,7 @@ impl Pallet { let stake = T::CoreStaking::stake(who)?; // release funds from core staking. - T::CoreStaking::force_release(who); + T::CoreStaking::migrate_to_virtual_staker(who); // transferring just released staked amount. This should never fail but if it does, it // indicates bad state and we abort. diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 3abc9e4a6d67..0f4903377f48 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -411,18 +411,14 @@ mod staking_integration { 100 )); - // if delegate calls Staking pallet directly with a different reward destination, it - // fails. + // update_payee to self fails. assert_noop!( - Staking::set_payee(RuntimeOrigin::signed(200), RewardDestination::Stash), + ::update_payee(&200, &200), StakingError::::RewardDestinationRestricted ); // passing correct reward destination works - assert_ok!(Staking::set_payee( - RuntimeOrigin::signed(200), - RewardDestination::Account(201) - )); + assert_ok!(::update_payee(&200, &201)); // amount is staked correctly assert!(eq_stake(200, 100, 100)); From 6f05a52049aa91dd56547a435202f5c06193bf0f Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 07:54:09 +0200 Subject: [PATCH 23/85] add test --- substrate/frame/staking/src/tests.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 67cc5b54b1a8..108cfe3dd0bc 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6971,11 +6971,26 @@ mod staking_unsafe { } #[test] - fn migrate_virtual_staker(){ - // TODO(ank4n) test migration integrity + fn migrate_virtual_staker() { + ExtBuilder::default().build_and_execute(|| { + // give some balance to 200 + Balances::make_free_balance_be(&200, 2000); + + // stake + assert_ok!(Staking::bond(RuntimeOrigin::signed(200), 1000, RewardDestination::Staked)); + assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 1000); + + // migrate them to virtual staker + ::migrate_to_virtual_staker(&200); + + // ensure the balance is not locked anymore + assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 0); + + // and they are marked as virtual stakers + assert_eq!(Pallet::::is_virtual_staker(&200), true); + }); } } - mod ledger { use super::*; From 4d00b043a7c1353b3c0254e73b1d0db682f6a852 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 07:58:03 +0200 Subject: [PATCH 24/85] remove benchmarking file --- .../delegated-staking/src/benchmarking.rs | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 substrate/frame/delegated-staking/src/benchmarking.rs diff --git a/substrate/frame/delegated-staking/src/benchmarking.rs b/substrate/frame/delegated-staking/src/benchmarking.rs deleted file mode 100644 index 808d19a5ce9a..000000000000 --- a/substrate/frame/delegated-staking/src/benchmarking.rs +++ /dev/null @@ -1,20 +0,0 @@ -// 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")] From 264f71a255972c1b35fb521a8fb95a25681f559c Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 08:24:36 +0200 Subject: [PATCH 25/85] small refactors --- substrate/frame/staking/src/mock.rs | 7 +++++++ substrate/frame/staking/src/tests.rs | 10 +++++----- substrate/primitives/staking/src/lib.rs | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 6db462c1a70f..11546e7971a1 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -598,6 +598,13 @@ pub(crate) fn bond_nominator(who: AccountId, val: Balance, target: Vec) { + // who is provided by another pallet in a real scenario. + System::inc_providers(&who); + assert_ok!(::virtual_bond(&who, val, &payee)); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target)); +} + /// Progress to the given block, triggering session and era changes as we progress. /// /// This will finalize the previous block, initialize up to the given block, essentially simulating diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 108cfe3dd0bc..5be7b9c999b9 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6947,24 +6947,24 @@ mod staking_unsafe { #[test] fn virtual_staker_cannot_bond_again() { ExtBuilder::default().build_and_execute(|| { - // 10 virtual bonds - assert_ok!(::virtual_bond(&10, 100, &11)); + // 200 virtual bonds + bond_virtual_nominator(200, 201, 500, vec![11, 21]); // Tries bonding again assert_noop!( - ::virtual_bond(&10, 20, &11), + ::virtual_bond(&200, 200, &201), Error::::AlreadyBonded ); // And again with a different reward destination. assert_noop!( - ::virtual_bond(&10, 20, &12), + ::virtual_bond(&200, 200, &202), Error::::AlreadyBonded ); // Direct bond is not allowed as well. assert_noop!( - ::bond(&10, 20, &12), + ::bond(&200, 200, &202), Error::::AlreadyBonded ); }); diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index fec915587d40..9939860d5da8 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -315,9 +315,9 @@ pub trait StakingInterface { /// These apis bypass some or all safety checks and should only be used if you know what you are /// doing. pub trait StakingUnsafe: StakingInterface { - /// Release all funds bonded for stake without unbonding the ledger. + /// Migrate an existing staker to a virtual staker. /// - /// Unsafe, should only used for migration of `nominator` to `virtual_nominator`. + /// It would release all funds held by the implementation pallet. fn migrate_to_virtual_staker(who: &Self::AccountId); /// Book-keep a new bond for `who` without applying any locks (hence virtual). From f9a52f181499a738a5afced6cba1983a8264c590 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 08:24:59 +0200 Subject: [PATCH 26/85] fmt --- substrate/frame/staking/src/mock.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 11546e7971a1..718ee697faed 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -598,7 +598,12 @@ pub(crate) fn bond_nominator(who: AccountId, val: Balance, target: Vec) { +pub(crate) fn bond_virtual_nominator( + who: AccountId, + payee: AccountId, + val: Balance, + target: Vec, +) { // who is provided by another pallet in a real scenario. System::inc_providers(&who); assert_ok!(::virtual_bond(&who, val, &payee)); From 7bf4d34ab6e3dafc7b81ffd56be19b141d748dc8 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 11:47:32 +0200 Subject: [PATCH 27/85] remove mod bench --- substrate/frame/delegated-staking/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 10992a0177ca..126433590792 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -138,9 +138,6 @@ use types::*; mod impls; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - use frame_support::{ pallet_prelude::*, traits::{ From 40a5132e23b778be85a326d84624838b0ff3c804 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 15:04:04 +0200 Subject: [PATCH 28/85] add pool migration function to Delegation Interface --- substrate/frame/delegated-staking/src/impls.rs | 10 ++++++++++ substrate/primitives/staking/src/lib.rs | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 56ad399ef494..c31f6a3028a7 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -106,6 +106,16 @@ impl DelegationInterface for Pallet { ) -> sp_runtime::DispatchResult { Pallet::::do_slash(agent.clone(), delegator.clone(), value, maybe_reporter) } + + 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(), + ) + } } impl OnStakingUpdate> for Pallet { diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 1cf889413291..adb4ac35bf10 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -536,6 +536,15 @@ pub trait DelegationInterface { value: Self::Balance, maybe_reporter: Option, ) -> sp_runtime::DispatchResult; + + /// Migrate an existing `Nominator` to `Agent` account. + /// + /// The implementation should ensure the `Nominator` account funds are moved to a temporary + /// `delegator` account from which funds can be later claimed as existing `Delegators`. + fn migrate_nominator_to_agent( + agent: &Self::AccountId, + reward_account: &Self::AccountId, + ) -> DispatchResult; } sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); From e8e498cfd682f2b4799bcc2a8f43e0ad21ddd619 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 15:51:53 +0200 Subject: [PATCH 29/85] add migrate delegation to delegation interface --- substrate/frame/delegated-staking/src/impls.rs | 12 ++++++++++++ substrate/primitives/staking/src/lib.rs | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index c31f6a3028a7..46f78700dc65 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -116,6 +116,18 @@ impl DelegationInterface for Pallet { reward_account.clone(), ) } + + fn migrate_delegation( + agent: &Self::AccountId, + delegator: &Self::AccountId, + value: Self::Balance, + ) -> DispatchResult { + Pallet::::claim_delegation( + RawOrigin::Signed(agent.clone()).into(), + delegator.clone(), + value, + ) + } } impl OnStakingUpdate> for Pallet { diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index adb4ac35bf10..bb57468aa456 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -539,12 +539,22 @@ pub trait DelegationInterface { /// Migrate an existing `Nominator` to `Agent` account. /// - /// The implementation should ensure the `Nominator` account funds are moved to a temporary - /// `delegator` account from which funds can be later claimed as existing `Delegators`. + /// 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", $); From 5ece9c7000635b5dfde7f2cd95c4d55b2e15a176 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 3 Apr 2024 17:46:59 +0200 Subject: [PATCH 30/85] dec provider if agent is killed --- substrate/frame/delegated-staking/src/lib.rs | 9 ++++++--- substrate/frame/delegated-staking/src/types.rs | 13 +++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 126433590792..80355eee5860 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -572,9 +572,12 @@ impl Pallet { // if we still do not have enough funds to release, abort. ensure!(agent.ledger.unclaimed_withdrawals >= amount, Error::::NotEnoughFunds); - // claim withdraw from agent. - agent.remove_unclaimed_withdraw(amount)?.save_or_kill()?; - + // claim withdraw from agent. Kill agent if no delegation left. + // TODO(ank4n): Ideally if there is a register, there should be an unregister that should + // clean up the agent. Need to improve this. + if agent.remove_unclaimed_withdraw(amount)?.save_or_kill()? { + let _ = frame_system::Pallet::::dec_providers(who).defensive(); + } // book keep delegation delegation.amount = delegation .amount diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index d48579226574..9eee2e61016d 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -263,8 +263,10 @@ impl Agent { /// Save self and remove if no delegation left. /// - /// Returns error if the delegate is in an unexpected state. - pub(crate) fn save_or_kill(self) -> Result<(), DispatchError> { + /// Returns: + /// - true if agent killed. + /// - error if the delegate is in an unexpected state. + pub(crate) fn save_or_kill(self) -> Result { let key = self.key; // see if delegate can be killed if self.ledger.total_delegated == Zero::zero() { @@ -274,11 +276,10 @@ impl Agent { Error::::BadState ); >::remove(key); - } else { - self.ledger.save(&key) + return Ok(true) } - - Ok(()) + self.ledger.save(&key); + Ok(false) } /// Reloads self from storage. From 23c65c46b42f95a85a605cafa331b187861a6d97 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 11:25:20 +0200 Subject: [PATCH 31/85] doc updates --- substrate/frame/delegated-staking/src/impls.rs | 3 +-- substrate/frame/delegated-staking/src/lib.rs | 14 ++++---------- substrate/primitives/staking/src/lib.rs | 8 +++++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 46f78700dc65..8e052223460e 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -16,8 +16,7 @@ // 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]. +//! Implementations of public traits, namely [DelegationInterface] and [OnStakingUpdate]. use super::*; use sp_staking::{DelegationInterface, OnStakingUpdate}; diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 80355eee5860..b683ed03001b 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -17,9 +17,9 @@ //! # Delegated Staking Pallet //! -//! This pallet implements [`sp_staking::DelegationInterface`] that extends [`StakingInterface`] -//! to support delegation of stake. It consumes [`Config::CoreStaking`] to provide primitive staking -//! functions and only implements the delegation features. +//! 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 @@ -68,12 +68,6 @@ //! 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::claim_delegation`]. //! -//! #### [Staking Interface](StakingInterface) -//! This pallet reimplements the staking interface as a wrapper implementation over -//! [Config::CoreStaking] to provide delegation based staking. Concretely, a pallet like -//! `NominationPools` can switch to this pallet as its Staking provider to support delegation based -//! staking from pool accounts, allowing its members to lock funds in their own account. -//! //! ## 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. @@ -89,7 +83,7 @@ //! [DelegationInterface::delegator_slash](sp_staking::DelegationInterface::delegator_slash). //! //! ## Migration from Nominator to Agent -//! More details [here](https://hackmd.io/@ak0n/np-delegated-staking-migration). +//! 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 over staking diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index bb57468aa456..6e128b460569 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -449,12 +449,14 @@ pub struct PagedExposureMetadata { pub page_count: Page, } -/// Extension of [`StakingInterface`] with delegation functionality. +/// Trait to provide delegation functionality for stakers. /// -/// Introduces two new actors: +/// Introduces two new terms to the staking system: /// - `Delegator`: An account that delegates funds to a `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. +/// funds to participate in the staking system. It can never use its own funds to stake. They +/// (virtually bond)[`StakingUnsafe::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. From 4f441d5e28dfabd1b89d88e8b124ff9f9f5d25e3 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 12:10:04 +0200 Subject: [PATCH 32/85] doc updates --- substrate/frame/delegated-staking/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index b683ed03001b..d76e292e2696 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -690,6 +690,7 @@ impl Pallet { ensure!(delegation.agent == agent_acc, Error::::NotAgent); ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); + // slash delegator let (mut credit, missing) = T::Currency::slash(&HoldReason::Delegating.into(), &delegator, amount); @@ -709,9 +710,11 @@ impl Pallet { let reward_payout: BalanceOf = T::CoreStaking::slash_reward_fraction() * actual_slash; let (reporter_reward, rest) = credit.split(reward_payout); + + // credit is the amount that we provide to `T::OnSlash`. credit = rest; - // fixme(ank4n): handle error + // reward reporter or drop it. let _ = T::Currency::resolve(&reporter, reporter_reward); } From 58b50c9f4e66e2009fdf0b37504982fbe8566de8 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 12:19:25 +0200 Subject: [PATCH 33/85] test for bonders cannot virtual bond --- substrate/frame/staking/src/pallet/mod.rs | 3 ++- substrate/frame/staking/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 8f18102175c0..6d6b993fe211 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -385,7 +385,8 @@ pub mod pallet { /// are expected to be keyless accounts and hence should not be allowed to mutate their ledger /// directly via this pallet. Instead, these accounts are managed by other pallets and accessed /// via low level apis. We keep track of them to do minimal integrity checks. - // TODO(ank4n): Can we keep this entry in `Ledger`? Worth a migration? + // TODO(ank4n): Can we keep this entry in `Ledger`? Or migrate later in conjunction with + // fungible migration? #[pallet::storage] pub type VirtualStakers = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 5be7b9c999b9..601779f4992c 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6970,6 +6970,23 @@ mod staking_unsafe { }); } + #[test] + fn normal_staker_cannot_virtual_bond() { + ExtBuilder::default().build_and_execute(|| { + // 101 is a nominator trying to virtual bond + assert_noop!( + ::virtual_bond(&101, 200, &102), + Error::::AlreadyBonded + ); + + // validator 21 tries to virtual bond + assert_noop!( + ::virtual_bond(&21, 200, &22), + Error::::AlreadyBonded + ); + }); + } + #[test] fn migrate_virtual_staker() { ExtBuilder::default().build_and_execute(|| { From b946c4a042f2152f68b4a05d91f8bfc780fe2c1a Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 12:38:51 +0200 Subject: [PATCH 34/85] virtual nominators receive rewards --- substrate/frame/staking/src/tests.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 601779f4992c..9300f5ec79fd 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -623,12 +623,8 @@ fn nominating_and_rewards_should_work() { )); assert_ok!(Staking::nominate(RuntimeOrigin::signed(1), vec![11, 21, 31])); - assert_ok!(Staking::bond( - RuntimeOrigin::signed(3), - 1000, - RewardDestination::Account(3) - )); - assert_ok!(Staking::nominate(RuntimeOrigin::signed(3), vec![11, 21, 41])); + // the second nominator is virtual. + bond_virtual_nominator(3, 333, 1000, vec![11, 21, 41]); // the total reward for era 0 let total_payout_0 = current_total_payout_for_duration(reward_time_per_era()); @@ -694,11 +690,10 @@ fn nominating_and_rewards_should_work() { ); // Nominator 3: has [400/1800 ~ 2/9 from 10] + [600/2200 ~ 3/11 from 21]'s reward. ==> // 2/9 + 3/11 + assert_eq!(Balances::total_balance(&3), initial_balance); + /// 333 is the reward destination for 3. assert_eq_error_rate!( - Balances::total_balance(&3), - initial_balance + (2 * payout_for_11 / 9 + 3 * payout_for_21 / 11), - 2, - ); + Balances::total_balance(&333), 2 * payout_for_11 / 9 + 3 * payout_for_21 / 11, 2); // Validator 11: got 800 / 1800 external stake => 8/18 =? 4/9 => Validator's share = 5/9 assert_eq_error_rate!( From 0aa9006542ab02f8a5b999552daf31101cbd9dea Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 13:30:52 +0200 Subject: [PATCH 35/85] virtual nominator slashing test --- substrate/frame/staking/src/mock.rs | 9 ++++- substrate/frame/staking/src/tests.rs | 58 +++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 718ee697faed..74762079101e 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -249,17 +249,22 @@ parameter_types! { pub static LedgerSlashPerEra: (BalanceOf, BTreeMap>) = (Zero::zero(), BTreeMap::new()); + pub static SlashObserver: BTreeMap> = BTreeMap::new(); } pub struct EventListenerMock; impl OnStakingUpdate for EventListenerMock { fn on_slash( - _pool_account: &AccountId, + pool_account: &AccountId, slashed_bonded: Balance, slashed_chunks: &BTreeMap, - _total_slashed: Balance, + total_slashed: Balance, ) { LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone())); + // update the observer. + let mut slash_observer = SlashObserver::get(); + slash_observer.insert(pool_account.clone(), total_slashed); + SlashObserver::set(slash_observer); } } diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 9300f5ec79fd..b2171d87b32f 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -691,7 +691,7 @@ fn nominating_and_rewards_should_work() { // Nominator 3: has [400/1800 ~ 2/9 from 10] + [600/2200 ~ 3/11 from 21]'s reward. ==> // 2/9 + 3/11 assert_eq!(Balances::total_balance(&3), initial_balance); - /// 333 is the reward destination for 3. + // 333 is the reward destination for 3. assert_eq_error_rate!( Balances::total_balance(&333), 2 * payout_for_11 / 9 + 3 * payout_for_21 / 11, 2); @@ -7002,6 +7002,62 @@ mod staking_unsafe { assert_eq!(Pallet::::is_virtual_staker(&200), true); }); } + + #[test] + fn virtual_nominators_are_lazily_slashed() { + ExtBuilder::default().build_and_execute(|| { + mock::start_active_era(1); + let slash_percent = Perbill::from_percent(5); + let initial_exposure = Staking::eras_stakers(active_era(), &11); + // 101 is a nominator for 11 + assert_eq!(initial_exposure.others.first().unwrap().who, 101); + // make 101 a virtual nominator + ::migrate_to_virtual_staker(&101); + // set payee different to self. + assert_ok!(::update_payee(&101, &102)); + + // cache values + let nominator_stake = Staking::ledger(101.into()).unwrap().active; + let nominator_balance = balances(&101).0; + let validator_stake = Staking::ledger(11.into()).unwrap().active; + let validator_balance = balances(&11).0; + let exposed_stake = initial_exposure.total; + let exposed_validator = initial_exposure.own; + let exposed_nominator = initial_exposure.others.first().unwrap().value; + + // 11 goes offline + on_offence_now( + &[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }], + &[slash_percent], + ); + + let slash_amount = slash_percent * exposed_stake; + let validator_share = + Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount; + let nominator_share = + Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount; + + // both slash amounts need to be positive for the test to make sense. + assert!(validator_share > 0); + assert!(nominator_share > 0); + + // both stakes must have been decreased pro-rata. + assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share); + assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share); + + // validator balance is slashed as usual + assert_eq!(balances(&11).0, validator_balance - validator_share); + // Because slashing happened. + assert!(is_disabled(11)); + + // but virtual nominator's balance is not slashed. + assert_eq!(Balances::free_balance(&101), nominator_balance); + // but slash is broadcasted to slash observers. + assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share); + + + }) + } } mod ledger { use super::*; From d242356751bbcd140b578d418a4fad67cc47ecde Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 13:32:22 +0200 Subject: [PATCH 36/85] fmt --- substrate/frame/staking/src/tests.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index b2171d87b32f..14c740be807c 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -693,7 +693,10 @@ fn nominating_and_rewards_should_work() { assert_eq!(Balances::total_balance(&3), initial_balance); // 333 is the reward destination for 3. assert_eq_error_rate!( - Balances::total_balance(&333), 2 * payout_for_11 / 9 + 3 * payout_for_21 / 11, 2); + Balances::total_balance(&333), + 2 * payout_for_11 / 9 + 3 * payout_for_21 / 11, + 2 + ); // Validator 11: got 800 / 1800 external stake => 8/18 =? 4/9 => Validator's share = 5/9 assert_eq_error_rate!( @@ -7042,8 +7045,14 @@ mod staking_unsafe { assert!(nominator_share > 0); // both stakes must have been decreased pro-rata. - assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share); - assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share); + assert_eq!( + Staking::ledger(101.into()).unwrap().active, + nominator_stake - nominator_share + ); + assert_eq!( + Staking::ledger(11.into()).unwrap().active, + validator_stake - validator_share + ); // validator balance is slashed as usual assert_eq!(balances(&11).0, validator_balance - validator_share); @@ -7054,8 +7063,6 @@ mod staking_unsafe { assert_eq!(Balances::free_balance(&101), nominator_balance); // but slash is broadcasted to slash observers. assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share); - - }) } } From 6bd5e2ca35ab46457c525f861e9b4ab82baaa545 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 14:29:35 +0200 Subject: [PATCH 37/85] fmt --- substrate/frame/delegated-staking/src/lib.rs | 13 +++- .../frame/delegated-staking/src/tests.rs | 73 ++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index d76e292e2696..99c75bcd7103 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -17,9 +17,10 @@ //! # 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. +//! 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 @@ -213,6 +214,8 @@ pub mod pallet { 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. @@ -685,8 +688,10 @@ impl Pallet { maybe_reporter: Option, ) -> DispatchResult { let agent = Agent::::from(&agent_acc)?; - let delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?; + // ensure there is something to slash + ensure!(agent.ledger.pending_slash > Zero::zero(), Error::::NothingToSlash); + let delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?; ensure!(delegation.agent == agent_acc, Error::::NotAgent); ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 0f4903377f48..d75bf6507dad 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -191,10 +191,81 @@ fn agent_restrictions() { }); } +use sp_staking::DelegationInterface; #[test] fn apply_pending_slash() { ExtBuilder::default().build_and_execute(|| { - // fixme(ank4n): add tests for apply_pending_slash + 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 = held_balance(&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!(held_balance(&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 + ); }); } From 2fb04c5bc6f70f1d637232645c561ec4a1167219 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 14:48:12 +0200 Subject: [PATCH 38/85] found a bug while unbonding --- substrate/frame/delegated-staking/src/mock.rs | 2 +- .../frame/delegated-staking/src/tests.rs | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index f98e0c5843bb..d8e7bebdafe9 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -130,7 +130,7 @@ impl pallet_staking::Config for Runtime { type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; type TargetList = pallet_staking::UseValidatorsMap; type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; - type MaxUnlockingChunks = ConstU32<32>; + type MaxUnlockingChunks = ConstU32<10>; type MaxControllersInDeprecationBatch = ConstU32<100>; type EventListeners = DelegatedStaking; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index d75bf6507dad..3754882b1f94 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -432,8 +432,9 @@ mod staking_integration { #[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); + setup_delegation_stake(agent, 201, (300..350).collect(), 10, 10); // verify withdraw not possible yet assert_noop!( @@ -441,10 +442,26 @@ mod staking_integration { Error::::NotEnoughFunds ); - // add new delegation that is not staked + // 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. + + // FIXME(ank4n): Since staking implicitly withdraws ledger, unclaimed withdrawals are + // not updated. This is bad since it can lead those funds to be re-bonded. + + // assert_eq!(get_agent(&agent).ledger.unclaimed_withdrawals, 8 * 10); - // 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_to_agent(RawOrigin::Signed(300.into()), @@ -531,15 +548,6 @@ mod staking_integration { }); } - #[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(|| { From 7962aac420f79eee700908296fa5769bdfdfbb50 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 15:22:34 +0200 Subject: [PATCH 39/85] test for unclaimed withdrawals --- .../frame/delegated-staking/src/impls.rs | 23 +++++------ substrate/frame/delegated-staking/src/lib.rs | 36 ++---------------- .../frame/delegated-staking/src/tests.rs | 38 +++++++++++-------- .../frame/delegated-staking/src/types.rs | 2 - substrate/primitives/staking/src/lib.rs | 11 ------ 5 files changed, 39 insertions(+), 71 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 8e052223460e..fc7cb1e41acc 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -79,17 +79,6 @@ impl DelegationInterface for Pallet { ) } - /// Withdraw unbonding funds until current era. - /// - /// Funds are moved to unclaimed_withdrawals register of the `AgentLedger`. - fn withdraw_unclaimed( - agent_acc: Self::AccountId, - num_slashing_spans: u32, - ) -> Result { - Pallet::::withdraw_unbonded(&agent_acc, num_slashing_spans) - .map(|agent| agent.ledger.total_delegated.is_zero()) - } - /// Returns true if the `Agent` have any slash pending to be applied. fn has_pending_slash(agent: &Self::AccountId) -> bool { Agent::::from(agent) @@ -144,4 +133,16 @@ impl OnStakingUpdate> for Pallet { }, }); } + + fn on_withdraw(stash: &T::AccountId, amount: BalanceOf) { + // if there is a withdraw to the agent, then add it to the unclaimed withdrawals. + if let Ok(agent) = Agent::::from(stash) { + let agent = agent.add_unclaimed_withdraw(amount).defensive(); + + // can't do anything if there is an overflow error. + if agent.is_ok() { + agent.expect("checked above; qed").save(); + } + } + } } diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 99c75bcd7103..632de3ea02c4 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -562,8 +562,10 @@ impl Pallet { // if we do not already have enough funds to be claimed, try withdraw some more. if agent.ledger.unclaimed_withdrawals < amount { - // get the updated agent. - agent = Self::withdraw_unbonded(who, num_slashing_spans)?; + // withdraw account. + let _ = T::CoreStaking::withdraw_unbonded(who.clone(), num_slashing_spans) + .map_err(|_| Error::::WithdrawFailed)?; + agent = agent.refresh()?; } // if we still do not have enough funds to release, abort. @@ -602,36 +604,6 @@ impl Pallet { Ok(()) } - fn withdraw_unbonded( - agent_acc: &T::AccountId, - num_slashing_spans: u32, - ) -> Result, DispatchError> { - let agent = Agent::::from(agent_acc)?; - let pre_total = T::CoreStaking::stake(agent_acc).defensive()?.total; - - let stash_killed: bool = - T::CoreStaking::withdraw_unbonded(agent_acc.clone(), num_slashing_spans) - .map_err(|_| Error::::WithdrawFailed)?; - - let maybe_post_total = T::CoreStaking::stake(agent_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 agent = agent.add_unclaimed_withdraw(new_withdrawn)?; - - agent.clone().save(); - - Ok(agent) - } - /// Migrates delegation of `amount` from `source` account to `destination` account. fn do_migrate_delegation( source_delegator: &T::AccountId, diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 3754882b1f94..280996afd2eb 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -434,7 +434,7 @@ mod staking_integration { ExtBuilder::default().build_and_execute(|| { start_era(1); let agent = 200; - setup_delegation_stake(agent, 201, (300..350).collect(), 10, 10); + setup_delegation_stake(agent, 201, (300..350).collect(), 100, 0); // verify withdraw not possible yet assert_noop!( @@ -454,25 +454,33 @@ mod staking_integration { // 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); - // FIXME(ank4n): Since staking implicitly withdraws ledger, unclaimed withdrawals are - // not updated. This is bad since it can lead those funds to be re-bonded. - - // 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 + )); - // fund(&300, 1000); - // assert_ok!(DelegatedStaking::delegate_to_agent(RawOrigin::Signed(300.into()), - // delegate, 100)); - // - // // verify unbonded balance - // assert_eq!(get_agent(&agent).available_to_bond(), 100); - // - // // withdraw works now without unbonding - // assert_ok!(DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 300, - // 100, 0)); assert_eq!(get_agent(&agent).available_to_bond(), 0); + assert_eq!(held_balance(&300), 100 - 80); }); } diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 9eee2e61016d..2fe49faf8464 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -283,8 +283,6 @@ impl Agent { } /// Reloads self from storage. - #[cfg(test)] - #[allow(unused)] pub(crate) fn refresh(&self) -> Result, DispatchError> { Self::from(&self.key) } diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 6e128b460569..f4fbdfa51129 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -512,17 +512,6 @@ pub trait DelegationInterface { amount: Self::Balance, ) -> DispatchResult; - /// Withdraw any unlocking funds in `CoreStaking` that can be claimed later by a delegator. - /// - /// `CoreStaking` has a limitation on maximum unlocking chunks at any given time. If the limit - /// is reached, we want to implicitly unlock these funds even though a delegator is not - /// present to claim it. Not doing this would block any unbonding until unlocking funds are - /// claimed. - fn withdraw_unclaimed( - agent: Self::AccountId, - num_slashing_spans: u32, - ) -> Result; - /// 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` From 8bd3bc3b52fd6be1e037d75d4a616d94e1f19f45 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 15:54:37 +0200 Subject: [PATCH 40/85] fix clippy error --- substrate/frame/staking/src/mock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 74762079101e..42578b1b7a30 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -263,7 +263,7 @@ impl OnStakingUpdate for EventListenerMock { LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone())); // update the observer. let mut slash_observer = SlashObserver::get(); - slash_observer.insert(pool_account.clone(), total_slashed); + slash_observer.insert(*pool_account, total_slashed); SlashObserver::set(slash_observer); } } From 15a9fc6ca3232eb1041cdae22950900985fd3f7c Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 16:10:10 +0200 Subject: [PATCH 41/85] fix clippy --- substrate/frame/delegated-staking/src/impls.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index fc7cb1e41acc..2eaa622ea51a 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -136,13 +136,10 @@ impl OnStakingUpdate> for Pallet { fn on_withdraw(stash: &T::AccountId, amount: BalanceOf) { // if there is a withdraw to the agent, then add it to the unclaimed withdrawals. - if let Ok(agent) = Agent::::from(stash) { - let agent = agent.add_unclaimed_withdraw(amount).defensive(); - + let _ = Agent::::from(stash) + .map(|agent| agent.add_unclaimed_withdraw(amount)) + .and_then(|agent| agent.save()) // can't do anything if there is an overflow error. - if agent.is_ok() { - agent.expect("checked above; qed").save(); - } - } + .defensive(); } } From 1a34d28912e497b811f8b032093df367cd079a6b Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 16:17:12 +0200 Subject: [PATCH 42/85] fix compile --- substrate/frame/delegated-staking/src/impls.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 2eaa622ea51a..8e3bca09e0d9 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -137,9 +137,10 @@ impl OnStakingUpdate> for Pallet { 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::::from(stash) - .map(|agent| agent.add_unclaimed_withdraw(amount)) - .and_then(|agent| agent.save()) - // can't do anything if there is an overflow error. - .defensive(); + // if not agent, we can ignore + .and_then(|agent| agent.add_unclaimed_withdraw(amount)) + // can't do anything if there is an overflow error. Just raise a defensive error. + .defensive() + .map(|agent| agent.save()); } } From 2a75dc5298eb7414cef7dce90f459859692629ef Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 4 Apr 2024 16:18:57 +0200 Subject: [PATCH 43/85] fix defensive trigger --- substrate/frame/delegated-staking/src/impls.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 8e3bca09e0d9..7edb6e000e16 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -137,10 +137,8 @@ impl OnStakingUpdate> for Pallet { 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::::from(stash) - // if not agent, we can ignore - .and_then(|agent| agent.add_unclaimed_withdraw(amount)) // can't do anything if there is an overflow error. Just raise a defensive error. - .defensive() + .and_then(|agent| agent.add_unclaimed_withdraw(amount).defensive()) .map(|agent| agent.save()); } } From 0bf4dba28f198d2d223b7d29a3281953b8e77cac Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 5 Apr 2024 01:00:09 +0200 Subject: [PATCH 44/85] pool does not have to transfer ed to proxy delegator --- substrate/frame/delegated-staking/src/lib.rs | 27 +++++++++---------- .../frame/delegated-staking/src/tests.rs | 7 ++--- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 632de3ea02c4..11babb4f39d8 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -470,18 +470,14 @@ impl Pallet { } 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()); - // Transfer minimum balance to proxy delegator. - T::Currency::transfer( - who, - &proxy_delegator, - T::Currency::minimum_balance(), - Preservation::Protect, - ) - .map_err(|_| Error::::NotEnoughFunds)?; + // 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)?; @@ -491,12 +487,10 @@ impl Pallet { // 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) + T::Currency::transfer(who, &proxy_delegator, stake.total, Preservation::Expendable) .map_err(|_| Error::::BadState)?; - Self::do_register_agent(who, reward_account); T::CoreStaking::update_payee(who, reward_account)?; - Self::do_delegate(&proxy_delegator, who, stake.total) } @@ -621,7 +615,6 @@ impl Pallet { // update delegations Delegation::::from(&source_delegation.agent, amount).save_or_kill(destination_delegator); - source_delegation .decrease_delegation(amount) .defensive_ok_or(Error::::BadState)? @@ -638,15 +631,19 @@ impl Pallet { 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( + let post_balance = T::Currency::transfer( source_delegator, destination_delegator, amount, - Preservation::Preserve, + 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::Delegating.into(), destination_delegator, amount)?; diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 280996afd2eb..292868cd3374 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -589,11 +589,8 @@ mod staking_integration { 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() - ); + // stake amount is transferred from delegate to proxy delegator account. + assert_eq!(Balances::free_balance(200), 5000 - staked_amount); assert_eq!(Staking::stake(&200).unwrap(), init_stake); assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); assert_eq!(get_agent(&200).available_to_bond(), 0); From 7800446012e9fcf4c67a7ef6713add90e763258a Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 9 Apr 2024 07:11:40 +0200 Subject: [PATCH 45/85] don't check for min balance when delegating to agent. pool should check this --- substrate/frame/delegated-staking/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 11babb4f39d8..54fd4fe06952 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -387,9 +387,6 @@ pub mod pallet { ) -> DispatchResult { let delegator = 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(&delegator, &agent), From 84fb918d788f3aa10d90205398b87302b3218f95 Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 9 Apr 2024 07:29:25 +0200 Subject: [PATCH 46/85] don't check for min balance --- substrate/frame/delegated-staking/src/lib.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 54fd4fe06952..8bf9a90bb2d3 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -397,13 +397,6 @@ pub mod pallet { // ensure agent is sane. ensure!(Self::is_agent(&agent), Error::::NotAgent); - let delegator_balance = T::Currency::reducible_balance( - &delegator, - Preservation::Preserve, - Fortitude::Polite, - ); - ensure!(delegator_balance >= amount, Error::::NotEnoughFunds); - // add to delegation Self::do_delegate(&delegator, &agent, amount)?; @@ -510,6 +503,8 @@ impl Pallet { amount: BalanceOf, ) -> DispatchResult { let mut ledger = AgentLedger::::get(agent).ok_or(Error::::NotAgent)?; + // try to hold the funds. + T::Currency::hold(&HoldReason::Delegating.into(), delegator, amount)?; let new_delegation_amount = if let Some(existing_delegation) = Delegation::::get(delegator) { @@ -527,8 +522,6 @@ impl Pallet { ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; ledger.save(agent); - T::Currency::hold(&HoldReason::Delegating.into(), delegator, amount)?; - Self::deposit_event(Event::::Delegated { agent: agent.clone(), delegator: delegator.clone(), From e55cea3c0fe1cd7f172d6b97df1558429464999d Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 9 Apr 2024 07:43:25 +0200 Subject: [PATCH 47/85] fix imports --- substrate/frame/delegated-staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 8bf9a90bb2d3..e73df73fbb92 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -142,7 +142,7 @@ use frame_support::{ }, Balanced, Inspect as FunInspect, Mutate as FunMutate, }, - tokens::{fungible::Credit, Fortitude, Precision, Preservation}, + tokens::{fungible::Credit, Precision, Preservation}, Defensive, DefensiveOption, Imbalance, OnUnbalanced, }, }; From 765278a103cddc1168c8ab2b599fcaed20935836 Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 9 Apr 2024 08:25:58 +0200 Subject: [PATCH 48/85] prdoc --- prdoc/pr_3904.prdoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 prdoc/pr_3904.prdoc diff --git a/prdoc/pr_3904.prdoc b/prdoc/pr_3904.prdoc new file mode 100644 index 000000000000..edfcddca8462 --- /dev/null +++ b/prdoc/pr_3904.prdoc @@ -0,0 +1,15 @@ +# 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 + From e46b01b357bd1a37f65130b29e0a1303d3383e97 Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 9 Apr 2024 08:30:41 +0200 Subject: [PATCH 49/85] prdoc --- prdoc/pr_3889.prdoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 prdoc/pr_3889.prdoc diff --git a/prdoc/pr_3889.prdoc b/prdoc/pr_3889.prdoc new file mode 100644 index 000000000000..b32ffcc214c0 --- /dev/null +++ b/prdoc/pr_3889.prdoc @@ -0,0 +1,14 @@ +# 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: Allow privileged virtual bond into pallet Staking + +doc: + - audience: Runtime Dev + description: | + Introduces a new low level API to allow privileged virtual bond into pallet Staking. This allows other pallets + to stake funds into staking pallet while managing the fund lock and unlocking process themselves. + +crates: + - name: pallet-staking + From 444dc76934d05b61069acc7b62a9e8530cc3db0f Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 12:33:22 +0200 Subject: [PATCH 50/85] add migrate to direct staker for testing --- substrate/frame/staking/src/pallet/impls.rs | 13 +++++++++++++ substrate/primitives/staking/src/lib.rs | 3 +++ 2 files changed, 16 insertions(+) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 029146935861..b3dbb7c49322 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1974,6 +1974,19 @@ impl sp_staking::StakingUnsafe for Pallet { Ok(()) } + + #[cfg(feature = "runtime-benchmarks")] + fn migrate_to_direct_staker(who: &Self::AccountId) { + assert!(VirtualStakers::::contains_key(who)); + let ledger = StakingLedger::::get(Stash(who.clone())).unwrap(); + T::Currency::set_lock( + crate::STAKING_ID, + who, + ledger.total, + frame_support::traits::WithdrawReasons::all(), + ); + VirtualStakers::::remove(who); + } } #[cfg(any(test, feature = "try-runtime"))] diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 9939860d5da8..0da02f190489 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -329,6 +329,9 @@ pub trait StakingUnsafe: StakingInterface { value: Self::Balance, payee: &Self::AccountId, ) -> DispatchResult; + + #[cfg(feature = "runtime-benchmarks")] + fn migrate_to_direct_staker(who: &Self::AccountId); } /// The amount of exposure for an era that an individual nominator has (susceptible to slashing). From b1312d7c967de49f7c76218a8aec6798a7101ac4 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 12:36:17 +0200 Subject: [PATCH 51/85] minor refactor --- substrate/frame/delegated-staking/src/lib.rs | 25 +++++++------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index e73df73fbb92..4e705dd9fac0 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -151,7 +151,7 @@ use sp_runtime::{ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero}, ArithmeticError, DispatchResult, RuntimeDebug, Saturating, }; -use sp_staking::{EraIndex, StakerStatus, StakingInterface, StakingUnsafe}; +use sp_staking::{EraIndex, StakingInterface, StakingUnsafe}; use sp_std::{convert::TryInto, prelude::*}; pub type BalanceOf = @@ -282,7 +282,7 @@ pub mod pallet { 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); + ensure!(!Self::is_direct_staker(&who), Error::::AlreadyStaking); // Reward account cannot be same as `agent` account. ensure!(reward_account != who, Error::::InvalidRewardDestination); @@ -313,7 +313,7 @@ pub mod pallet { ensure!(!Self::is_agent(&who), Error::::NotAllowed); // and they should already be a nominator in `CoreStaking`. - ensure!(Self::is_direct_nominator(&who), Error::::NotAllowed); + ensure!(Self::is_direct_staker(&who), Error::::NotAllowed); // Reward account cannot be same as `agent` account. ensure!(reward_account != who, Error::::InvalidRewardDestination); @@ -359,7 +359,7 @@ pub mod pallet { // Ensure delegator is sane. ensure!(!Self::is_agent(&delegator), Error::::NotAllowed); ensure!(!Self::is_delegator(&delegator), Error::::NotAllowed); - ensure!(Self::not_direct_staker(&delegator), Error::::AlreadyStaking); + ensure!(!Self::is_direct_staker(&delegator), Error::::AlreadyStaking); // ensure agent is sane. ensure!(Self::is_agent(&agent), Error::::NotAgent); @@ -392,7 +392,7 @@ pub mod pallet { Delegation::::can_delegate(&delegator, &agent), Error::::InvalidDelegation ); - ensure!(Self::not_direct_staker(&delegator), Error::::AlreadyStaking); + ensure!(!Self::is_direct_staker(&delegator), Error::::AlreadyStaking); // ensure agent is sane. ensure!(Self::is_agent(&agent), Error::::NotAgent); @@ -437,16 +437,9 @@ impl Pallet { >::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) + /// Returns true if who is already staking on [`Config::CoreStaking`]. + fn is_direct_staker(who: &T::AccountId) -> bool { + T::CoreStaking::status(who).is_ok() } fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) { @@ -721,7 +714,7 @@ impl Pallet { ensure!( matches!( T::CoreStaking::status(&agent).expect("agent should be bonded"), - StakerStatus::Nominator(_) | StakerStatus::Idle + sp_staking::StakerStatus::Nominator(_) | sp_staking::StakerStatus::Idle ), "agent should be bonded and not validator" ); From 9079d8227d1f4f33e9f5ec377fe2fa450ef106a8 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 12:33:22 +0200 Subject: [PATCH 52/85] add migrate to direct staker for testing --- substrate/frame/staking/src/pallet/impls.rs | 13 +++++++++++++ substrate/primitives/staking/src/lib.rs | 3 +++ 2 files changed, 16 insertions(+) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 029146935861..b3dbb7c49322 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1974,6 +1974,19 @@ impl sp_staking::StakingUnsafe for Pallet { Ok(()) } + + #[cfg(feature = "runtime-benchmarks")] + fn migrate_to_direct_staker(who: &Self::AccountId) { + assert!(VirtualStakers::::contains_key(who)); + let ledger = StakingLedger::::get(Stash(who.clone())).unwrap(); + T::Currency::set_lock( + crate::STAKING_ID, + who, + ledger.total, + frame_support::traits::WithdrawReasons::all(), + ); + VirtualStakers::::remove(who); + } } #[cfg(any(test, feature = "try-runtime"))] diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 9939860d5da8..0da02f190489 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -329,6 +329,9 @@ pub trait StakingUnsafe: StakingInterface { value: Self::Balance, payee: &Self::AccountId, ) -> DispatchResult; + + #[cfg(feature = "runtime-benchmarks")] + fn migrate_to_direct_staker(who: &Self::AccountId); } /// The amount of exposure for an era that an individual nominator has (susceptible to slashing). From 616dfa78b6e813732f530ae9b579761604c89359 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 12:40:21 +0200 Subject: [PATCH 53/85] doc comment --- substrate/primitives/staking/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 0da02f190489..e482bf1b85ac 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -330,6 +330,9 @@ pub trait StakingUnsafe: StakingInterface { payee: &Self::AccountId, ) -> DispatchResult; + /// Migrate a virtual staker to a direct staker. + /// + /// Only used for testing. #[cfg(feature = "runtime-benchmarks")] fn migrate_to_direct_staker(who: &Self::AccountId); } From afe8c87b5a234af4fbf418913c2bb681fd9173c5 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 13:59:37 +0200 Subject: [PATCH 54/85] pr feedback --- substrate/frame/staking/src/pallet/impls.rs | 2 +- substrate/frame/staking/src/pallet/mod.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index b3dbb7c49322..ff7c95131506 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -169,7 +169,7 @@ impl Pallet { ledger.total += extra; ledger.active += extra; - // Last check: the new active amount of ledger must be more than ED. + // last check: the new active amount of ledger must be more than ED. ensure!(ledger.active >= T::Currency::minimum_balance(), Error::::InsufficientBond); // NOTE: ledger must be updated prior to calling `Self::weight_of`. diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 6d6b993fe211..31f4ddeb32d4 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -871,7 +871,7 @@ pub mod pallet { CannotRestoreLedger, /// Provided reward destination is not allowed. RewardDestinationRestricted, - /// Not enough funds available to withdraw + /// Not enough funds available to withdraw. NotEnoughFunds, } @@ -1308,7 +1308,9 @@ pub mod pallet { Error::::ControllerDeprecated ); - ledger.set_payee(payee)?; + let _ = ledger + .set_payee(payee) + .defensive_proof("ledger was retrieved from storage, thus its bonded; qed.")?; Ok(()) } From e7d65e2debcb233c79b8e6285bceb584d8a19152 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 14:24:01 +0200 Subject: [PATCH 55/85] add try state checks for virtual ledger --- substrate/frame/staking/src/pallet/impls.rs | 25 ++++++++++++++++----- substrate/frame/staking/src/tests.rs | 2 ++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index ff7c95131506..f0c779c1bfb2 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -2109,17 +2109,30 @@ impl Pallet { /// Invariants: /// * Stake consistency: ledger.total == ledger.active + sum(ledger.unlocking). /// * The ledger's controller and stash matches the associated `Bonded` tuple. - /// * Staking locked funds for every bonded stash should be the same as its ledger's total. + /// * Staking locked funds for every bonded stash (non virtual stakers) should be the same as + /// its ledger's total. + /// * For virtual stakers, locked funds should be zero and payee should be non-stash account. /// * Staking ledger and bond are not corrupted. fn check_ledgers() -> Result<(), TryRuntimeError> { Bonded::::iter() - .filter(|(stash, _)| !VirtualStakers::::contains_key(stash)) .map(|(stash, ctrl)| { // ensure locks consistency. - ensure!( - Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok), - "bond, ledger and/or staking lock inconsistent for a bonded stash." - ); + if VirtualStakers::::contains_key(stash.clone()) { + ensure!(T::Currency::balance_locked(crate::STAKING_ID, &stash) == Zero::zero(), "virtual stakers should not have any locked balance"); + ensure!(>::get(stash.clone()).unwrap() == stash.clone(), "stash and controller should be same"); + ensure!(Ledger::::get(stash.clone()).unwrap().stash == stash, "ledger corrupted for virtual staker"); + let reward_destination = >::get(stash.clone()).unwrap(); + if let RewardDestination::Account(payee) = reward_destination { + ensure!(payee != stash.clone(), "reward destination should not be same as stash for virtual staker"); + } else { + return Err(DispatchError::Other("reward destination must be of account variant for virtual staker")); + } + } else { + ensure!( + Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok), + "bond, ledger and/or staking lock inconsistent for a bonded stash." + ); + } // ensure ledger consistency. Self::ensure_ledger_consistent(ctrl) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 14c740be807c..3837aad05193 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6997,6 +6997,8 @@ mod staking_unsafe { // migrate them to virtual staker ::migrate_to_virtual_staker(&200); + // payee needs to be updated to a non-stash account. + assert_ok!(::update_payee(&200, &201)); // ensure the balance is not locked anymore assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 0); From 474c1dee3b718366e4c302249eeb0407d1787afb Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 11 Apr 2024 14:24:22 +0200 Subject: [PATCH 56/85] fmt --- substrate/frame/staking/src/pallet/impls.rs | 24 ++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index f0c779c1bfb2..81f4a9ead883 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -2118,14 +2118,28 @@ impl Pallet { .map(|(stash, ctrl)| { // ensure locks consistency. if VirtualStakers::::contains_key(stash.clone()) { - ensure!(T::Currency::balance_locked(crate::STAKING_ID, &stash) == Zero::zero(), "virtual stakers should not have any locked balance"); - ensure!(>::get(stash.clone()).unwrap() == stash.clone(), "stash and controller should be same"); - ensure!(Ledger::::get(stash.clone()).unwrap().stash == stash, "ledger corrupted for virtual staker"); + ensure!( + T::Currency::balance_locked(crate::STAKING_ID, &stash) == Zero::zero(), + "virtual stakers should not have any locked balance" + ); + ensure!( + >::get(stash.clone()).unwrap() == stash.clone(), + "stash and controller should be same" + ); + ensure!( + Ledger::::get(stash.clone()).unwrap().stash == stash, + "ledger corrupted for virtual staker" + ); let reward_destination = >::get(stash.clone()).unwrap(); if let RewardDestination::Account(payee) = reward_destination { - ensure!(payee != stash.clone(), "reward destination should not be same as stash for virtual staker"); + ensure!( + payee != stash.clone(), + "reward destination should not be same as stash for virtual staker" + ); } else { - return Err(DispatchError::Other("reward destination must be of account variant for virtual staker")); + return Err(DispatchError::Other( + "reward destination must be of account variant for virtual staker", + )); } } else { ensure!( From 6a019d60bc59a195e68fb093bf57057a4dc97dc3 Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:00:23 +0100 Subject: [PATCH 57/85] Update substrate/frame/delegated-staking/src/lib.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gonçalo Pestana --- substrate/frame/delegated-staking/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 4e705dd9fac0..564b2ed80fe8 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -275,11 +275,8 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - // Existing `agent` cannot register again. - ensure!(!Self::is_agent(&who), Error::::NotAllowed); - - // A delegator cannot become an `agent`. - ensure!(!Self::is_delegator(&who), Error::::NotAllowed); + // 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); From 8d481a63b9b085a9c0320c59a5e353ae733edcb1 Mon Sep 17 00:00:00 2001 From: Ankan <10196091+Ank4n@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:21:36 +0200 Subject: [PATCH 58/85] Apply suggestions from code review Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: georgepisaltu <52418509+georgepisaltu@users.noreply.github.com> --- substrate/frame/delegated-staking/src/impls.rs | 2 +- substrate/frame/delegated-staking/src/lib.rs | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 7edb6e000e16..531118353d0f 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -16,7 +16,7 @@ // 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]. +//! Implementations of public traits, namely [`DelegationInterface`] and [`OnStakingUpdate`]. use super::*; use sp_staking::{DelegationInterface, OnStakingUpdate}; diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 564b2ed80fe8..40c7dec0bdf1 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -24,7 +24,7 @@ //! //! 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 -//! multi location such as a parachain or a smart contract. +//! `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 @@ -87,9 +87,9 @@ //! 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 over 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 layer in between of Nomination Pool and Staking Pallet. Technically, these +//! 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 @@ -146,7 +146,6 @@ use frame_support::{ Defensive, DefensiveOption, Imbalance, OnUnbalanced, }, }; - use sp_runtime::{ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero}, ArithmeticError, DispatchResult, RuntimeDebug, Saturating, @@ -254,9 +253,9 @@ pub mod pallet { 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. + // 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`. /// From a884846703b8dab4eae593beca30abf658a333ae Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 18:29:23 +0200 Subject: [PATCH 59/85] safe maths --- substrate/frame/staking/src/pallet/impls.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 81f4a9ead883..c14849ba8ba6 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -36,9 +36,10 @@ use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; use pallet_session::historical; use sp_runtime::{ traits::{ - Bounded, CheckedSub, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero, + Bounded, CheckedAdd, CheckedSub, Convert, One, SaturatedConversion, Saturating, + StaticLookup, Zero, }, - Perbill, Percent, + ArithmeticError, Perbill, Percent, }; use sp_staking::{ currency_to_vote::CurrencyToVote, @@ -163,12 +164,12 @@ impl Pallet { additional.min( T::Currency::free_balance(stash) .checked_sub(&ledger.total) - .ok_or(sp_runtime::ArithmeticError::Overflow)?, + .ok_or(ArithmeticError::Overflow)?, ) }; - ledger.total += extra; - ledger.active += extra; + ledger.total = ledger.total.checked_add(&extra).ok_or(ArithmeticError::Overflow)?; + ledger.active = ledger.active.checked_add(&extra).ok_or(ArithmeticError::Overflow)?; // last check: the new active amount of ledger must be more than ED. ensure!(ledger.active >= T::Currency::minimum_balance(), Error::::InsufficientBond); From d4633c2c0bf1a1a45ce326cf0b24d8e668a51dd7 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 18:39:15 +0200 Subject: [PATCH 60/85] fix docs based on feedback --- substrate/frame/staking/src/mock.rs | 5 ++++- substrate/frame/staking/src/pallet/impls.rs | 19 ++++++++++--------- substrate/primitives/staking/src/lib.rs | 7 ++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 42578b1b7a30..76a4a579e0ab 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -609,8 +609,11 @@ pub(crate) fn bond_virtual_nominator( val: Balance, target: Vec, ) { - // who is provided by another pallet in a real scenario. + // In a real scenario, `who` is a keyless account managed by another pallet which provides for + // it. System::inc_providers(&who); + + // Bond who virtually. assert_ok!(::virtual_bond(&who, val, &payee)); assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target)); } diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index c14849ba8ba6..24ea647d8a6b 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1948,29 +1948,30 @@ impl sp_staking::StakingUnsafe for Pallet { VirtualStakers::::insert(who, ()); } + /// Virtually bonds `keyless_who` to `payee` with `value`. + /// + /// The payee must not be the same as the `keyless_who`. fn virtual_bond( - who: &Self::AccountId, + keyless_who: &Self::AccountId, value: Self::Balance, payee: &Self::AccountId, ) -> DispatchResult { - if StakingLedger::::is_bonded(StakingAccount::Stash(who.clone())) { + if StakingLedger::::is_bonded(StakingAccount::Stash(keyless_who.clone())) { return Err(Error::::AlreadyBonded.into()) } // check if payee not same as who. - ensure!(who != payee, Error::::RewardDestinationRestricted); + ensure!(keyless_who != payee, Error::::RewardDestinationRestricted); // mark this pallet as consumer of `who`. - frame_system::Pallet::::inc_consumers(&who).map_err(|_| Error::::BadState)?; + frame_system::Pallet::::inc_consumers(&keyless_who).map_err(|_| Error::::BadState)?; // mark who as a virtual staker. - VirtualStakers::::insert(who, ()); + VirtualStakers::::insert(keyless_who, ()); - Self::deposit_event(Event::::Bonded { stash: who.clone(), amount: value }); - let ledger = StakingLedger::::new(who.clone(), value); + Self::deposit_event(Event::::Bonded { stash: keyless_who.clone(), amount: value }); + let ledger = StakingLedger::::new(keyless_who.clone(), value); - // You're auto-bonded forever, here. We might improve this by only bonding when - // you actually validate/nominate and remove once you unbond __everything__. ledger.bond(RewardDestination::Account(payee.clone()))?; Ok(()) diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index e482bf1b85ac..662d71c7e3d1 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -320,10 +320,11 @@ pub trait StakingUnsafe: StakingInterface { /// It would release all funds held by the implementation pallet. fn migrate_to_virtual_staker(who: &Self::AccountId); - /// Book-keep a new bond for `who` without applying any locks (hence virtual). + /// Book-keep a new bond for `keyless_who` without applying any locks (hence virtual). /// - /// It is important that who is a keyless account and therefore cannot interact with staking - /// pallet directly. Caller is responsible for ensuring the passed amount is locked and valid. + /// It is important that `keyless_who` is a keyless account and therefore cannot interact with + /// staking pallet directly. Caller is responsible for ensuring the passed amount is locked and + /// valid. fn virtual_bond( keyless_who: &Self::AccountId, value: Self::Balance, From c013cded09529f9a395736d421abc1a265a45f4d Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 18:41:20 +0200 Subject: [PATCH 61/85] rename StakingUnsafe to StakingUnchecked --- substrate/frame/staking/src/mock.rs | 2 +- substrate/frame/staking/src/pallet/impls.rs | 2 +- substrate/frame/staking/src/tests.rs | 22 ++++++++++----------- substrate/primitives/staking/src/lib.rs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 76a4a579e0ab..4df616d91370 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -614,7 +614,7 @@ pub(crate) fn bond_virtual_nominator( System::inc_providers(&who); // Bond who virtually. - assert_ok!(::virtual_bond(&who, val, &payee)); + assert_ok!(::virtual_bond(&who, val, &payee)); assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target)); } diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 24ea647d8a6b..3c1925cdfac4 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1942,7 +1942,7 @@ impl StakingInterface for Pallet { } } -impl sp_staking::StakingUnsafe for Pallet { +impl sp_staking::StakingUnchecked for Pallet { fn migrate_to_virtual_staker(who: &Self::AccountId) { T::Currency::remove_lock(crate::STAKING_ID, who); VirtualStakers::::insert(who, ()); diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 3837aad05193..aff7e40efc43 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6847,9 +6847,9 @@ mod staking_interface { } } -mod staking_unsafe { +mod staking_unchecked { use frame_support::traits::InspectLockableCurrency; - use sp_staking::{Stake, StakingInterface, StakingUnsafe}; + use sp_staking::{Stake, StakingInterface, StakingUnchecked}; use super::*; @@ -6860,7 +6860,7 @@ mod staking_unsafe { assert_eq!(Balances::free_balance(10), 1); // 10 can bond more than its balance amount since we do not require lock for virtual // bonding. - assert_ok!(::virtual_bond(&10, 100, &15)); + assert_ok!(::virtual_bond(&10, 100, &15)); // nothing is locked on 10. assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0); // adding more balance does not lock anything as well. @@ -6927,12 +6927,12 @@ mod staking_unsafe { ExtBuilder::default().build_and_execute(|| { // cannot set payee to self assert_noop!( - ::virtual_bond(&10, 100, &10), + ::virtual_bond(&10, 100, &10), Error::::RewardDestinationRestricted ); // to another account works - assert_ok!(::virtual_bond(&10, 100, &11)); + assert_ok!(::virtual_bond(&10, 100, &11)); // cannot set via set_payee as well. assert_noop!( @@ -6950,13 +6950,13 @@ mod staking_unsafe { // Tries bonding again assert_noop!( - ::virtual_bond(&200, 200, &201), + ::virtual_bond(&200, 200, &201), Error::::AlreadyBonded ); // And again with a different reward destination. assert_noop!( - ::virtual_bond(&200, 200, &202), + ::virtual_bond(&200, 200, &202), Error::::AlreadyBonded ); @@ -6973,13 +6973,13 @@ mod staking_unsafe { ExtBuilder::default().build_and_execute(|| { // 101 is a nominator trying to virtual bond assert_noop!( - ::virtual_bond(&101, 200, &102), + ::virtual_bond(&101, 200, &102), Error::::AlreadyBonded ); // validator 21 tries to virtual bond assert_noop!( - ::virtual_bond(&21, 200, &22), + ::virtual_bond(&21, 200, &22), Error::::AlreadyBonded ); }); @@ -6996,7 +6996,7 @@ mod staking_unsafe { assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 1000); // migrate them to virtual staker - ::migrate_to_virtual_staker(&200); + ::migrate_to_virtual_staker(&200); // payee needs to be updated to a non-stash account. assert_ok!(::update_payee(&200, &201)); @@ -7017,7 +7017,7 @@ mod staking_unsafe { // 101 is a nominator for 11 assert_eq!(initial_exposure.others.first().unwrap().who, 101); // make 101 a virtual nominator - ::migrate_to_virtual_staker(&101); + ::migrate_to_virtual_staker(&101); // set payee different to self. assert_ok!(::update_payee(&101, &102)); diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 662d71c7e3d1..ad6cc6e2f4ff 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -314,7 +314,7 @@ pub trait StakingInterface { /// /// These apis bypass some or all safety checks and should only be used if you know what you are /// doing. -pub trait StakingUnsafe: StakingInterface { +pub trait StakingUnchecked: StakingInterface { /// Migrate an existing staker to a virtual staker. /// /// It would release all funds held by the implementation pallet. From ac3cadf78bf4a13093258ea801b118a693f4e61b Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 18:58:55 +0200 Subject: [PATCH 62/85] use static mutate --- substrate/frame/staking/src/mock.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 4df616d91370..385ce656f446 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -261,10 +261,7 @@ impl OnStakingUpdate for EventListenerMock { total_slashed: Balance, ) { LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone())); - // update the observer. - let mut slash_observer = SlashObserver::get(); - slash_observer.insert(*pool_account, total_slashed); - SlashObserver::set(slash_observer); + SlashObserver::mutate(|map| map.insert(*pool_account, total_slashed)); } } From 0537e9c431d5f718b65367da1c9bd69761c9a8ae Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 19:19:19 +0200 Subject: [PATCH 63/85] Rename StakingUnsafe to StakingUnchecked --- substrate/frame/delegated-staking/src/lib.rs | 10 +++++----- substrate/frame/delegated-staking/src/types.rs | 8 ++++---- substrate/primitives/staking/src/lib.rs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 40c7dec0bdf1..587607797c9e 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -150,7 +150,7 @@ use sp_runtime::{ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero}, ArithmeticError, DispatchResult, RuntimeDebug, Saturating, }; -use sp_staking::{EraIndex, StakingInterface, StakingUnsafe}; +use sp_staking::{EraIndex, StakingInterface, StakingUnchecked}; use sp_std::{convert::TryInto, prelude::*}; pub type BalanceOf = @@ -186,7 +186,7 @@ pub mod pallet { type RuntimeHoldReason: From; /// Core staking implementation. - type CoreStaking: StakingUnsafe, AccountId = Self::AccountId>; + type CoreStaking: StakingUnchecked, AccountId = Self::AccountId>; } #[pallet::error] @@ -263,7 +263,7 @@ pub mod pallet { /// them. The `Agent` can then use the delegated funds to stake to [`Config::CoreStaking`]. /// /// Implementation note: This function allows any account to become an agent. It is - /// important though that accounts that call [`StakingUnsafe::virtual_bond`] are keyless + /// 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 @@ -506,7 +506,7 @@ impl Pallet { amount }; - Delegation::::from(agent, new_delegation_amount).save_or_kill(delegator); + Delegation::::new(agent, new_delegation_amount).save_or_kill(delegator); ledger.total_delegated = ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; ledger.save(agent); @@ -593,7 +593,7 @@ impl Pallet { ); // update delegations - Delegation::::from(&source_delegation.agent, amount).save_or_kill(destination_delegator); + Delegation::::new(&source_delegation.agent, amount).save_or_kill(destination_delegator); source_delegation .decrease_delegation(amount) .defensive_ok_or(Error::::BadState)? diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 2fe49faf8464..56f06e82de1f 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -47,7 +47,7 @@ impl Delegation { } /// Create and return a new delegation instance. - pub(crate) fn from(agent: &T::AccountId, amount: BalanceOf) -> Self { + pub(crate) fn new(agent: &T::AccountId, amount: BalanceOf) -> Self { Delegation { agent: agent.clone(), amount } } @@ -57,7 +57,7 @@ impl Delegation { /// 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.clone()) + .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), @@ -67,14 +67,14 @@ impl Delegation { /// 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.agent, updated_delegation)) + Some(Delegation::new(&self.agent, 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.agent, updated_delegation)) + Some(Delegation::new(&self.agent, updated_delegation)) } /// Save self to storage. If the delegation amount is zero, remove the delegation. diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index cabe066326c4..ea2fbc7e2cb8 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -462,7 +462,7 @@ pub struct PagedExposureMetadata { /// - `Delegator`: An account that delegates funds to a `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)[`StakingUnsafe::virtual_bond`] into the staking system and can also be termed +/// (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 From fb5d64cd405d23c6bd70d478740db1f18df5d572 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 19:38:05 +0200 Subject: [PATCH 64/85] pr feedbacks --- .../frame/delegated-staking/src/impls.rs | 6 ++--- substrate/frame/delegated-staking/src/lib.rs | 24 +++++++++---------- substrate/frame/delegated-staking/src/mock.rs | 4 ++-- .../frame/delegated-staking/src/tests.rs | 1 + .../frame/delegated-staking/src/types.rs | 9 +++---- substrate/primitives/staking/src/lib.rs | 4 ++-- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index 531118353d0f..f4bec0700089 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -27,7 +27,7 @@ impl DelegationInterface for Pallet { /// Effective balance of the `Agent` account. fn agent_balance(who: &Self::AccountId) -> Self::Balance { - Agent::::from(who) + Agent::::get(who) .map(|agent| agent.ledger.effective_balance()) .unwrap_or_default() } @@ -81,7 +81,7 @@ impl DelegationInterface for Pallet { /// Returns true if the `Agent` have any slash pending to be applied. fn has_pending_slash(agent: &Self::AccountId) -> bool { - Agent::::from(agent) + Agent::::get(agent) .map(|d| !d.ledger.pending_slash.is_zero()) .unwrap_or(false) } @@ -136,7 +136,7 @@ impl OnStakingUpdate> for Pallet { 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::::from(stash) + 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 index 587607797c9e..79878c5e7c3a 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -87,14 +87,14 @@ //! 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. +//! 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 @@ -474,7 +474,7 @@ impl Pallet { } fn do_bond(agent_acc: &T::AccountId, amount: BalanceOf) -> DispatchResult { - let agent = Agent::::from(agent_acc)?; + 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"); @@ -526,7 +526,7 @@ impl Pallet { amount: BalanceOf, num_slashing_spans: u32, ) -> DispatchResult { - let mut agent = Agent::::from(who)?; + let mut agent = Agent::::get(who)?; let mut delegation = Delegation::::get(delegator).ok_or(Error::::NotDelegator)?; // make sure delegation to be released is sound. @@ -635,7 +635,7 @@ impl Pallet { amount: BalanceOf, maybe_reporter: Option, ) -> DispatchResult { - let agent = Agent::::from(&agent_acc)?; + let agent = Agent::::get(&agent_acc)?; // ensure there is something to slash ensure!(agent.ledger.pending_slash > Zero::zero(), Error::::NothingToSlash); @@ -681,7 +681,7 @@ impl Pallet { /// Total balance that is available for stake. Includes already staked amount. #[cfg(test)] pub(crate) fn stakeable_balance(who: &T::AccountId) -> BalanceOf { - Agent::::from(who) + Agent::::get(who) .map(|agent| agent.ledger.stakeable_balance()) .unwrap_or_default() } diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index d8e7bebdafe9..1c4a1e2b2744 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -280,7 +280,7 @@ pub(crate) fn setup_delegation_stake( // sanity checks assert_eq!(DelegatedStaking::stakeable_balance(&agent), delegated_amount); - assert_eq!(Agent::::from(&agent).unwrap().available_to_bond(), 0); + assert_eq!(Agent::::get(&agent).unwrap().available_to_bond(), 0); delegated_amount } @@ -295,7 +295,7 @@ pub(crate) fn eq_stake(who: AccountId, total: Balance, active: Balance) -> bool } pub(crate) fn get_agent(agent: &AccountId) -> Agent { - Agent::::from(agent).expect("delegate should exist") + Agent::::get(agent).expect("delegate should exist") } #[allow(unused)] diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 292868cd3374..55a97e8e3619 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -305,6 +305,7 @@ mod staking_integration { Balances::balance_on_hold(&HoldReason::Delegating.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); diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 56f06e82de1f..0ecb0732cee8 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -165,7 +165,7 @@ pub struct Agent { impl Agent { /// Get `Agent` from storage if it exists or return an error. - pub(crate) fn from(agent: &T::AccountId) -> Result, DispatchError> { + pub(crate) fn get(agent: &T::AccountId) -> Result, DispatchError> { let ledger = AgentLedger::::get(agent).ok_or(Error::::NotAgent)?; Ok(Agent { key: agent.clone(), ledger }) } @@ -284,7 +284,7 @@ impl Agent { /// Reloads self from storage. pub(crate) fn refresh(&self) -> Result, DispatchError> { - Self::from(&self.key) + Self::get(&self.key) } /// Balance of `Agent` that is not bonded. @@ -298,10 +298,7 @@ impl Agent { let net_balance = self.ledger.effective_balance(); - defensive_assert!( - net_balance >= bonded_stake, - "cannot be bonded with more than the agent 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/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index ea2fbc7e2cb8..80d2695d27ec 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -462,8 +462,8 @@ pub struct PagedExposureMetadata { /// - `Delegator`: An account that delegates funds to a `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`. +/// (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. From bb93c8c5f12af5be30f09ab84cdddd7e1cd6ee73 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 20:17:10 +0200 Subject: [PATCH 65/85] pr feedbacks --- substrate/frame/delegated-staking/src/lib.rs | 45 +++++++++---------- substrate/frame/delegated-staking/src/mock.rs | 2 +- .../frame/delegated-staking/src/tests.rs | 19 +++++--- .../frame/delegated-staking/src/types.rs | 10 ++--- substrate/primitives/staking/src/lib.rs | 4 +- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 79878c5e7c3a..f516df90278e 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -226,7 +226,7 @@ pub mod pallet { pub enum HoldReason { /// Funds held for stake delegation to another account. #[codec(index = 0)] - Delegating, + StakingDelegation, } #[pallet::event] @@ -290,7 +290,7 @@ pub mod pallet { /// Migrate from a `Nominator` account to `Agent` account. /// /// The origin needs to - /// - be a `Nominator` with `CoreStaking`, + /// - be a `Nominator` with [`Config::CoreStaking`], /// - not already an `Agent`, /// - have enough funds to transfer existential deposit to a delegator account created for /// the migration. @@ -305,11 +305,8 @@ pub mod pallet { reward_account: T::AccountId, ) -> DispatchResult { let who = ensure_signed(origin)?; - // ensure who is not already an agent. - ensure!(!Self::is_agent(&who), Error::::NotAllowed); - - // and they should already be a nominator in `CoreStaking`. - ensure!(Self::is_direct_staker(&who), Error::::NotAllowed); + // ensure who is a staker in `CpreStaking` but not already an agent. + ensure!(!Self::is_agent(&who) && Self::is_direct_staker(&who), Error::::NotAllowed); // Reward account cannot be same as `agent` account. ensure!(reward_account != who, Error::::InvalidRewardDestination); @@ -375,7 +372,8 @@ pub mod pallet { /// /// Conditions: /// - Delegators cannot delegate to more than one agent. - /// - The `agent` account should already be registered as such. See [`Self::register_agent`] + /// - The `agent` account should already be registered as such. See + /// [`Self::register_agent`]. pub fn delegate_to_agent( origin: OriginFor, agent: T::AccountId, @@ -407,8 +405,6 @@ pub mod pallet { fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { Self::do_try_state() } - - fn integrity_test() {} } } @@ -420,7 +416,7 @@ impl Pallet { /// 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) + T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), who) } /// Returns true if who is registered as an `Agent`. @@ -439,7 +435,7 @@ impl Pallet { } fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) { - AgentLedger::::new(reward_account).save(who); + 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. @@ -493,7 +489,7 @@ impl Pallet { ) -> DispatchResult { let mut ledger = AgentLedger::::get(agent).ok_or(Error::::NotAgent)?; // try to hold the funds. - T::Currency::hold(&HoldReason::Delegating.into(), delegator, amount)?; + T::Currency::hold(&HoldReason::StakingDelegation.into(), delegator, amount)?; let new_delegation_amount = if let Some(existing_delegation) = Delegation::::get(delegator) { @@ -506,10 +502,10 @@ impl Pallet { amount }; - Delegation::::new(agent, new_delegation_amount).save_or_kill(delegator); + Delegation::::new(agent, new_delegation_amount).update_or_kill(delegator); ledger.total_delegated = ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - ledger.save(agent); + ledger.update(agent); Self::deposit_event(Event::::Delegated { agent: agent.clone(), @@ -547,7 +543,7 @@ impl Pallet { // claim withdraw from agent. Kill agent if no delegation left. // TODO(ank4n): Ideally if there is a register, there should be an unregister that should // clean up the agent. Need to improve this. - if agent.remove_unclaimed_withdraw(amount)?.save_or_kill()? { + if agent.remove_unclaimed_withdraw(amount)?.update_or_kill()? { let _ = frame_system::Pallet::::dec_providers(who).defensive(); } // book keep delegation @@ -557,10 +553,10 @@ impl Pallet { .defensive_ok_or(ArithmeticError::Overflow)?; // remove delegator if nothing delegated anymore - delegation.save_or_kill(delegator); + delegation.update_or_kill(delegator); let released = T::Currency::release( - &HoldReason::Delegating.into(), + &HoldReason::StakingDelegation.into(), delegator, amount, Precision::BestEffort, @@ -593,15 +589,16 @@ impl Pallet { ); // update delegations - Delegation::::new(&source_delegation.agent, amount).save_or_kill(destination_delegator); + Delegation::::new(&source_delegation.agent, amount) + .update_or_kill(destination_delegator); source_delegation .decrease_delegation(amount) .defensive_ok_or(Error::::BadState)? - .save_or_kill(source_delegator); + .update_or_kill(source_delegator); // release funds from source let released = T::Currency::release( - &HoldReason::Delegating.into(), + &HoldReason::StakingDelegation.into(), source_delegator, amount, Precision::BestEffort, @@ -624,7 +621,7 @@ impl Pallet { } // hold the funds again in the new delegator account. - T::Currency::hold(&HoldReason::Delegating.into(), destination_delegator, amount)?; + T::Currency::hold(&HoldReason::StakingDelegation.into(), destination_delegator, amount)?; Ok(()) } @@ -645,7 +642,7 @@ impl Pallet { // slash delegator let (mut credit, missing) = - T::Currency::slash(&HoldReason::Delegating.into(), &delegator, amount); + T::Currency::slash(&HoldReason::StakingDelegation.into(), &delegator, amount); defensive_assert!(missing.is_zero(), "slash should have been fully applied"); @@ -657,7 +654,7 @@ impl Pallet { delegation .decrease_delegation(actual_slash) .ok_or(ArithmeticError::Overflow)? - .save_or_kill(&delegator); + .update_or_kill(&delegator); if let Some(reporter) = maybe_reporter { let reward_payout: BalanceOf = diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 1c4a1e2b2744..5dffed115843 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -300,7 +300,7 @@ pub(crate) fn get_agent(agent: &AccountId) -> Agent { #[allow(unused)] pub(crate) fn held_balance(who: &AccountId) -> Balance { - Balances::balance_on_hold(&HoldReason::Delegating.into(), who) + Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), who) } parameter_types! { diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 55a97e8e3619..320cf6d0aec7 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -47,7 +47,11 @@ fn create_a_agent_with_first_delegator() { // verify assert!(DelegatedStaking::is_agent(&agent)); assert_eq!(DelegatedStaking::stakeable_balance(&agent), 100); - assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator), 100); + assert_eq!( + Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &delegator), + 100 + ); + assert_eq!(DelegatedStaking::held_balance_of(&delegator), 100); }); } @@ -113,7 +117,7 @@ fn create_multiple_delegators() { 100 )); // Balance of 100 held on delegator account for delegating to the agent. - assert_eq!(Balances::balance_on_hold(&HoldReason::Delegating.into(), &i), 100); + assert_eq!(Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &i), 100); } // verify @@ -302,7 +306,7 @@ mod staking_integration { )); delegated_balance += 100; assert_eq!( - Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator), + Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &delegator), 100 ); assert_eq!(DelegatedStaking::delegator_balance(&delegator), 100); @@ -587,7 +591,7 @@ mod staking_integration { // verify all went well let mut expected_proxy_delegated_amount = staked_amount; assert_eq!( - Balances::balance_on_hold(&HoldReason::Delegating.into(), &proxy_delegator), + Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &proxy_delegator), expected_proxy_delegated_amount ); // stake amount is transferred from delegate to proxy delegator account. @@ -609,12 +613,15 @@ mod staking_integration { delegator_share )); assert_eq!( - Balances::balance_on_hold(&HoldReason::Delegating.into(), &delegator), + Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), &delegator), delegator_share ); expected_proxy_delegated_amount -= delegator_share; assert_eq!( - Balances::balance_on_hold(&HoldReason::Delegating.into(), &proxy_delegator), + Balances::balance_on_hold( + &HoldReason::StakingDelegation.into(), + &proxy_delegator + ), expected_proxy_delegated_amount ); diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 0ecb0732cee8..5432d680bb0b 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -78,7 +78,7 @@ impl Delegation { } /// Save self to storage. If the delegation amount is zero, remove the delegation. - pub(crate) fn save_or_kill(self, key: &T::AccountId) { + pub(crate) fn update_or_kill(self, key: &T::AccountId) { // Clean up if no delegation left. if self.amount == Zero::zero() { >::remove(key); @@ -131,7 +131,7 @@ impl AgentLedger { } /// Save self to storage with the given key. - pub(crate) fn save(self, key: &T::AccountId) { + pub(crate) fn update(self, key: &T::AccountId) { >::insert(key, self) } @@ -258,7 +258,7 @@ impl Agent { /// Save self to storage. pub(crate) fn save(self) { let key = self.key; - self.ledger.save(&key) + self.ledger.update(&key) } /// Save self and remove if no delegation left. @@ -266,7 +266,7 @@ impl Agent { /// Returns: /// - true if agent killed. /// - error if the delegate is in an unexpected state. - pub(crate) fn save_or_kill(self) -> Result { + 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() { @@ -278,7 +278,7 @@ impl Agent { >::remove(key); return Ok(true) } - self.ledger.save(&key); + self.ledger.update(&key); Ok(false) } diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 80d2695d27ec..84c683330048 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -459,7 +459,7 @@ pub struct PagedExposureMetadata { /// Trait to provide delegation functionality for stakers. /// /// Introduces two new terms to the staking system: -/// - `Delegator`: An account that delegates funds to a `Agent`. +/// - `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 @@ -525,7 +525,7 @@ pub trait DelegationInterface { /// has an unbounded number of delegators, immediate slashing is not possible. fn has_pending_slash(agent: &Self::AccountId) -> bool; - /// Apply a pending slash to a `Agent` by slashing `value` from `delegator`. + /// Apply a pending slash to an `Agent` by slashing `value` from `delegator`. /// /// If a reporter is provided, the reporter will receive a fraction of the slash as reward. fn delegator_slash( From cba250e032567c6a9be1e6037cb4b2cc6fea6af0 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 20:36:02 +0200 Subject: [PATCH 66/85] feedbacks --- substrate/frame/delegated-staking/src/lib.rs | 5 +++++ substrate/frame/delegated-staking/src/mock.rs | 9 ++------- substrate/frame/delegated-staking/src/tests.rs | 13 +++++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index f516df90278e..0469fe038290 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -46,6 +46,11 @@ //! 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. diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 5dffed115843..7f91872addfd 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -15,12 +15,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{self as delegated_staking, types::Agent, HoldReason}; +use crate::{self as delegated_staking, types::Agent}; use frame_support::{ assert_ok, derive_impl, pallet_prelude::*, parameter_types, - traits::{fungible::InspectHold, ConstU64, Currency}, + traits::{ConstU64, Currency}, PalletId, }; @@ -298,11 +298,6 @@ pub(crate) fn get_agent(agent: &AccountId) -> Agent { Agent::::get(agent).expect("delegate should exist") } -#[allow(unused)] -pub(crate) fn held_balance(who: &AccountId) -> Balance { - Balances::balance_on_hold(&HoldReason::StakingDelegation.into(), who) -} - parameter_types! { static ObservedEventsDelegatedStaking: usize = 0; } diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 320cf6d0aec7..3bc9afe40355 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -21,6 +21,7 @@ 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_a_agent_with_first_delegator() { @@ -195,7 +196,6 @@ fn agent_restrictions() { }); } -use sp_staking::DelegationInterface; #[test] fn apply_pending_slash() { ExtBuilder::default().build_and_execute(|| { @@ -239,7 +239,7 @@ fn apply_pending_slash() { // balance before slash let initial_pending_slash = get_agent(&agent).ledger.pending_slash; assert!(initial_pending_slash > 0); - let unslashed_balance = held_balance(&i); + let unslashed_balance = DelegatedStaking::held_balance_of(&i); let slash = unslashed_balance / 2; // slash half of delegator's delegation. assert_ok!(::delegator_slash( @@ -250,7 +250,7 @@ fn apply_pending_slash() { )); // balance after slash. - assert_eq!(held_balance(&i), unslashed_balance - 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. @@ -341,15 +341,16 @@ mod staking_integration { DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), 301, 50, 0), Error::::NotEnoughFunds ); - // assert_noop!(DelegatedStaking::release_delegation(RawOrigin::Signed(agent).into(), - // 200, 50, 0), Error::::NotAllowed); active and total stake remains same + assert!(eq_stake(agent, total_staked, total_staked)); // 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)); @@ -485,7 +486,7 @@ mod staking_integration { 0 )); - assert_eq!(held_balance(&300), 100 - 80); + assert_eq!(DelegatedStaking::held_balance_of(&300), 100 - 80); }); } From 9897437ccd9ea05dd2e83f54b336f7a43e05f3b9 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 17 Apr 2024 20:53:04 +0200 Subject: [PATCH 67/85] feedback --- substrate/frame/delegated-staking/src/lib.rs | 24 ++++++++++--------- .../frame/delegated-staking/src/tests.rs | 2 -- .../frame/delegated-staking/src/types.rs | 15 +----------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 0469fe038290..98e067295c77 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -539,6 +539,7 @@ impl Pallet { // withdraw account. let _ = 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()?; } @@ -584,7 +585,7 @@ impl Pallet { destination_delegator: &T::AccountId, amount: BalanceOf, ) -> DispatchResult { - let source_delegation = + let mut source_delegation = Delegators::::get(source_delegator).defensive_ok_or(Error::::BadState)?; // some checks that must have already been checked before. @@ -596,10 +597,13 @@ impl Pallet { // update delegations Delegation::::new(&source_delegation.agent, amount) .update_or_kill(destination_delegator); - source_delegation - .decrease_delegation(amount) - .defensive_ok_or(Error::::BadState)? - .update_or_kill(source_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( @@ -641,7 +645,7 @@ impl Pallet { // ensure there is something to slash ensure!(agent.ledger.pending_slash > Zero::zero(), Error::::NothingToSlash); - let delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?; + let mut delegation = >::get(&delegator).ok_or(Error::::NotDelegator)?; ensure!(delegation.agent == agent_acc, Error::::NotAgent); ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); @@ -655,11 +659,9 @@ impl Pallet { // remove the applied slashed amount from agent. agent.remove_slash(actual_slash).save(); - - delegation - .decrease_delegation(actual_slash) - .ok_or(ArithmeticError::Overflow)? - .update_or_kill(&delegator); + 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 = diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 3bc9afe40355..ce584906d1b0 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -342,8 +342,6 @@ mod staking_integration { Error::::NotEnoughFunds ); - assert!(eq_stake(agent, total_staked, total_staked)); - // 305 wants to unbond 50 in era 2, withdrawable in era 5. assert_ok!(Staking::unbond(RawOrigin::Signed(agent).into(), 50)); diff --git a/substrate/frame/delegated-staking/src/types.rs b/substrate/frame/delegated-staking/src/types.rs index 5432d680bb0b..0bfc23281dfe 100644 --- a/substrate/frame/delegated-staking/src/types.rs +++ b/substrate/frame/delegated-staking/src/types.rs @@ -64,19 +64,6 @@ impl Delegation { ) } - /// 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::new(&self.agent, 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::new(&self.agent, updated_delegation)) - } - /// 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. @@ -283,7 +270,7 @@ impl Agent { } /// Reloads self from storage. - pub(crate) fn refresh(&self) -> Result, DispatchError> { + pub(crate) fn refresh(self) -> Result, DispatchError> { Self::get(&self.key) } From e31866e842e65e8713f488bc14dbcb935630bcad Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 19 Apr 2024 15:46:50 +0200 Subject: [PATCH 68/85] inline locking --- substrate/frame/staking/src/ledger.rs | 12 ++++++++++-- substrate/frame/staking/src/pallet/impls.rs | 20 -------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs index 01531703c316..dd9ce11c4d2c 100644 --- a/substrate/frame/staking/src/ledger.rs +++ b/substrate/frame/staking/src/ledger.rs @@ -188,8 +188,16 @@ impl StakingLedger { return Err(Error::::NotStash) } - // update lock on stash based on ledger. - Pallet::::update_lock(&self.stash, self.total).map_err(|_| Error::::BadState)?; + // We skip locking virtual stakers. + if !Pallet::::is_virtual_staker(&self.stash) { + // for direct stakers, update lock on stash based on ledger. + T::Currency::set_lock( + STAKING_ID, + &self.stash, + self.total, + frame_support::traits::WithdrawReasons::all(), + ); + } Ledger::::insert( &self.controller().ok_or_else(|| { diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 3c1925cdfac4..0c0ef0dbf463 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1173,26 +1173,6 @@ impl Pallet { pub(crate) fn is_virtual_staker(who: &T::AccountId) -> bool { VirtualStakers::::contains_key(who) } - - /// Update the lock for a staker. - /// - /// For virtual stakers, it is no-op. - pub(crate) fn update_lock( - who: &T::AccountId, - amount: BalanceOf, - ) -> sp_runtime::DispatchResult { - // Skip locking virtual stakers. They are handled by external pallets. - if !Self::is_virtual_staker(who) { - T::Currency::set_lock( - crate::STAKING_ID, - who, - amount, - frame_support::traits::WithdrawReasons::all(), - ); - } - - Ok(()) - } } impl Pallet { From 078cf5fe72079072b21b3823ab45e5deb17c2714 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 19 Apr 2024 16:25:25 +0200 Subject: [PATCH 69/85] optimize stash kill --- substrate/frame/staking/src/ledger.rs | 9 ++++++--- substrate/frame/staking/src/mock.rs | 4 +++- substrate/frame/staking/src/pallet/mod.rs | 2 -- substrate/frame/staking/src/tests.rs | 8 ++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs index dd9ce11c4d2c..67a86b86226c 100644 --- a/substrate/frame/staking/src/ledger.rs +++ b/substrate/frame/staking/src/ledger.rs @@ -263,12 +263,15 @@ impl StakingLedger { let controller = >::get(stash).ok_or(Error::::NotStash)?; >::get(&controller).ok_or(Error::::NotController).map(|ledger| { - T::Currency::remove_lock(STAKING_ID, &ledger.stash); Ledger::::remove(controller); - >::remove(&stash); >::remove(&stash); - >::remove(&stash); + + // kill virtual staker if it exists. + if >::take(&stash).is_none() { + // if not virtual staker, clear locks. + T::Currency::remove_lock(STAKING_ID, &ledger.stash); + } Ok(()) })? diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 385ce656f446..b46b863c016e 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -261,7 +261,9 @@ impl OnStakingUpdate for EventListenerMock { total_slashed: Balance, ) { LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone())); - SlashObserver::mutate(|map| map.insert(*pool_account, total_slashed)); + SlashObserver::mutate(|map| { + map.insert(*pool_account, map.get(pool_account).unwrap_or(&0) + total_slashed) + }); } } diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 31f4ddeb32d4..76ddad6f1359 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -385,8 +385,6 @@ pub mod pallet { /// are expected to be keyless accounts and hence should not be allowed to mutate their ledger /// directly via this pallet. Instead, these accounts are managed by other pallets and accessed /// via low level apis. We keep track of them to do minimal integrity checks. - // TODO(ank4n): Can we keep this entry in `Ledger`? Or migrate later in conjunction with - // fungible migration? #[pallet::storage] pub type VirtualStakers = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index aff7e40efc43..87f6fd424bd7 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -27,7 +27,7 @@ use frame_support::{ assert_noop, assert_ok, assert_storage_noop, dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo}, pallet_prelude::*, - traits::{Currency, Get, ReservableCurrency}, + traits::{Currency, Get, InspectLockableCurrency, ReservableCurrency}, }; use mock::*; @@ -1891,7 +1891,7 @@ fn reap_stash_works() { .balance_factor(10) .build_and_execute(|| { // given - assert_eq!(Balances::free_balance(11), 10 * 1000); + assert_eq!(Balances::balance_locked(STAKING_ID, &11), 10 * 1000); assert_eq!(Staking::bonded(&11), Some(11)); assert!(>::contains_key(&11)); @@ -1917,6 +1917,8 @@ fn reap_stash_works() { assert!(!>::contains_key(&11)); assert!(!>::contains_key(&11)); assert!(!>::contains_key(&11)); + // lock is removed. + assert_eq!(Balances::balance_locked(STAKING_ID, &11), 0); }); } @@ -6848,7 +6850,6 @@ mod staking_interface { } mod staking_unchecked { - use frame_support::traits::InspectLockableCurrency; use sp_staking::{Stake, StakingInterface, StakingUnchecked}; use super::*; @@ -7546,7 +7547,6 @@ mod ledger { mod ledger_recovery { use super::*; - use frame_support::traits::InspectLockableCurrency; #[test] fn inspect_recovery_ledger_simple_works() { From be76a6bd71611be2968b23cbd199c1e4c0ba7618 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 22 Apr 2024 11:35:10 +0200 Subject: [PATCH 70/85] pr feedbacks --- substrate/frame/delegated-staking/src/lib.rs | 33 ++++++++++++------- .../frame/delegated-staking/src/tests.rs | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 98e067295c77..8a3b95b45303 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -46,12 +46,12 @@ //! 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 +//! ### 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 +//! ### 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. //! @@ -206,7 +206,7 @@ pub mod pallet { /// /// Possible issues are /// 1) Cannot delegate to self, - /// 2) Cannot delegate to multiple delegates, + /// 2) Cannot delegate to multiple delegates. InvalidDelegation, /// The account does not have enough funds to perform the operation. NotEnoughFunds, @@ -310,8 +310,11 @@ pub mod pallet { reward_account: T::AccountId, ) -> DispatchResult { let who = ensure_signed(origin)?; - // ensure who is a staker in `CpreStaking` but not already an agent. - ensure!(!Self::is_agent(&who) && Self::is_direct_staker(&who), Error::::NotAllowed); + // 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); @@ -342,7 +345,7 @@ pub mod pallet { /// 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 `pxoxy_delegator` account to `delegator` + /// Internally, it moves some delegations from `proxy_delegator` account to `delegator` /// account and reapplying the holds. pub fn claim_delegation( origin: OriginFor, @@ -367,7 +370,7 @@ pub mod pallet { let balance_remaining = Self::held_balance_of(&proxy_delegator); ensure!(balance_remaining >= amount, Error::::NotEnoughFunds); - Self::do_migrate_delegation(&proxy_delegator, &delegator, amount) + Self::migrate_delegation(&proxy_delegator, &delegator, amount) } /// Delegate given `amount` of tokens to an `Agent` account. @@ -396,7 +399,7 @@ pub mod pallet { // ensure agent is sane. ensure!(Self::is_agent(&agent), Error::::NotAgent); - // add to delegation + // add to delegation. Self::do_delegate(&delegator, &agent, amount)?; // bond the newly delegated amount to `CoreStaking`. @@ -419,7 +422,7 @@ impl Pallet { T::PalletId::get().into_sub_account_truncating((account_type, agent.clone())) } - /// Balance of a delegator that is delegated. + /// Held balance of a delegator. pub(crate) fn held_balance_of(who: &T::AccountId) -> BalanceOf { T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), who) } @@ -439,6 +442,7 @@ impl Pallet { 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); @@ -449,6 +453,7 @@ impl Pallet { 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); @@ -474,6 +479,7 @@ impl Pallet { Self::do_delegate(&proxy_delegator, who, stake.total) } + /// Bond `amount` to `agent_acc` in [`Config::CoreStaking`]. fn do_bond(agent_acc: &T::AccountId, amount: BalanceOf) -> DispatchResult { let agent = Agent::::get(agent_acc)?; @@ -487,6 +493,7 @@ impl Pallet { } } + /// Delegate `amount` from `delegator` to `agent`. fn do_delegate( delegator: &T::AccountId, agent: &T::AccountId, @@ -521,6 +528,7 @@ impl Pallet { Ok(()) } + /// Release `amount` of delegated funds from `agent` to `delegator`. fn do_release( who: &T::AccountId, delegator: &T::AccountId, @@ -547,8 +555,8 @@ impl Pallet { ensure!(agent.ledger.unclaimed_withdrawals >= amount, Error::::NotEnoughFunds); // claim withdraw from agent. Kill agent if no delegation left. - // TODO(ank4n): Ideally if there is a register, there should be an unregister that should - // clean up the agent. Need to improve this. + // 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()? { let _ = frame_system::Pallet::::dec_providers(who).defensive(); } @@ -580,7 +588,7 @@ impl Pallet { } /// Migrates delegation of `amount` from `source` account to `destination` account. - fn do_migrate_delegation( + fn migrate_delegation( source_delegator: &T::AccountId, destination_delegator: &T::AccountId, amount: BalanceOf, @@ -635,6 +643,7 @@ impl Pallet { 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, diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index ce584906d1b0..37b42480a0a0 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -24,7 +24,7 @@ use pallet_staking::Error as StakingError; use sp_staking::DelegationInterface; #[test] -fn create_a_agent_with_first_delegator() { +fn create_an_agent_with_first_delegator() { ExtBuilder::default().build_and_execute(|| { let agent: AccountId = 200; let reward_account: AccountId = 201; From c6070d807e9b7924ecf117e5d6d7f12fc0170b31 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 22 Apr 2024 12:26:52 +0200 Subject: [PATCH 71/85] check stash is killed before cleaning up agent --- substrate/frame/delegated-staking/src/lib.rs | 27 +++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 8a3b95b45303..6ba25ca3d3b9 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -543,23 +543,42 @@ impl Pallet { ensure!(delegation.amount >= amount, Error::::NotEnoughFunds); // if we do not already have enough funds to be claimed, try withdraw some more. - if agent.ledger.unclaimed_withdrawals < amount { + // keep track if we killed the staker in the process. + let stash_killed = if agent.ledger.unclaimed_withdrawals < amount { // withdraw account. - let _ = T::CoreStaking::withdraw_unbonded(who.clone(), num_slashing_spans) + 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. + // 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 From 4b2daa2238a9eda0630ca213553ecd6a088e027e Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 24 Apr 2024 14:43:07 +0200 Subject: [PATCH 72/85] withdraw delegation requires num slashing spans --- substrate/frame/delegated-staking/src/impls.rs | 4 ++-- substrate/primitives/staking/src/lib.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index f4bec0700089..a1c441b17227 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -69,13 +69,13 @@ impl DelegationInterface for Pallet { delegator: &Self::AccountId, agent: &Self::AccountId, amount: Self::Balance, + num_slashing_spans: u32, ) -> DispatchResult { - // fixme(ank4n): Can this not require slashing spans? Pallet::::release_delegation( RawOrigin::Signed(agent.clone()).into(), delegator.clone(), amount, - 0, + num_slashing_spans, ) } diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 84c683330048..08dc29ba3efa 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -517,6 +517,7 @@ pub trait DelegationInterface { 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. From 0e17037c0a398c5bc99f7a8abdebe52721bcb88a Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 24 Apr 2024 14:52:23 +0200 Subject: [PATCH 73/85] separate trait for migration --- .../frame/delegated-staking/src/impls.rs | 7 ++++++- substrate/primitives/staking/src/lib.rs | 20 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index a1c441b17227..acfa89e6245b 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -19,7 +19,7 @@ //! Implementations of public traits, namely [`DelegationInterface`] and [`OnStakingUpdate`]. use super::*; -use sp_staking::{DelegationInterface, OnStakingUpdate}; +use sp_staking::{DelegationInterface, DelegationMigrator, OnStakingUpdate}; impl DelegationInterface for Pallet { type Balance = BalanceOf; @@ -94,6 +94,11 @@ impl DelegationInterface for Pallet { ) -> 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, diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 08dc29ba3efa..cc317d9e4556 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -534,7 +534,25 @@ pub trait DelegationInterface { delegator: &Self::AccountId, value: Self::Balance, maybe_reporter: Option, - ) -> sp_runtime::DispatchResult; + ) -> 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. /// From 4e3b8cd992951abbb14ac7b541453e49cf5e1672 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 24 Apr 2024 15:08:05 +0200 Subject: [PATCH 74/85] add a separate slash reward fraction for agent slashing --- substrate/frame/delegated-staking/src/lib.rs | 9 ++++++--- substrate/frame/delegated-staking/src/mock.rs | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 6ba25ca3d3b9..bc849c859f6f 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -153,7 +153,7 @@ use frame_support::{ }; use sp_runtime::{ traits::{AccountIdConversion, CheckedAdd, CheckedSub, Zero}, - ArithmeticError, DispatchResult, RuntimeDebug, Saturating, + ArithmeticError, DispatchResult, Perbill, RuntimeDebug, Saturating, }; use sp_staking::{EraIndex, StakingInterface, StakingUnchecked}; use sp_std::{convert::TryInto, prelude::*}; @@ -187,6 +187,10 @@ pub mod pallet { /// 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; @@ -692,8 +696,7 @@ impl Pallet { delegation.update_or_kill(&delegator); if let Some(reporter) = maybe_reporter { - let reward_payout: BalanceOf = - T::CoreStaking::slash_reward_fraction() * actual_slash; + 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`. diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 7f91872addfd..6a5717533410 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -139,12 +139,14 @@ impl pallet_staking::Config for Runtime { 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; } From b1b1e1d972783316be9241eb9a5600ba1ae23431 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 12:26:01 +0200 Subject: [PATCH 75/85] better docs pr feedback --- substrate/primitives/staking/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index cc317d9e4556..c7045508cea3 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -528,7 +528,8 @@ pub trait DelegationInterface { /// Apply a pending slash to an `Agent` by slashing `value` from `delegator`. /// - /// If a reporter is provided, the reporter will receive a fraction of the slash as reward. + /// 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, From 1e6cdadd2351418a68584a878971d7f2fd91fbf8 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 12:29:34 +0200 Subject: [PATCH 76/85] rename claim delegation to migrate delegation --- substrate/frame/delegated-staking/src/impls.rs | 2 +- substrate/frame/delegated-staking/src/lib.rs | 10 +++++----- substrate/frame/delegated-staking/src/tests.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/substrate/frame/delegated-staking/src/impls.rs b/substrate/frame/delegated-staking/src/impls.rs index acfa89e6245b..b1945b0ce376 100644 --- a/substrate/frame/delegated-staking/src/impls.rs +++ b/substrate/frame/delegated-staking/src/impls.rs @@ -115,7 +115,7 @@ impl DelegationMigrator for Pallet { delegator: &Self::AccountId, value: Self::Balance, ) -> DispatchResult { - Pallet::::claim_delegation( + Pallet::::migrate_delegation( RawOrigin::Signed(agent.clone()).into(), delegator.clone(), value, diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index bc849c859f6f..5911a2c55ccb 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -342,8 +342,8 @@ pub mod pallet { Self::do_release(&who, &delegator, amount, num_slashing_spans) } - /// Claim delegated funds that are held in `proxy_delegator` to the claiming delegator's - /// account. If successful, the specified funds will be delegated directly from `delegator` + /// 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 @@ -351,7 +351,7 @@ pub mod pallet { /// /// Internally, it moves some delegations from `proxy_delegator` account to `delegator` /// account and reapplying the holds. - pub fn claim_delegation( + pub fn migrate_delegation( origin: OriginFor, delegator: T::AccountId, amount: BalanceOf, @@ -374,7 +374,7 @@ pub mod pallet { let balance_remaining = Self::held_balance_of(&proxy_delegator); ensure!(balance_remaining >= amount, Error::::NotEnoughFunds); - Self::migrate_delegation(&proxy_delegator, &delegator, amount) + Self::do_migrate_delegation(&proxy_delegator, &delegator, amount) } /// Delegate given `amount` of tokens to an `Agent` account. @@ -611,7 +611,7 @@ impl Pallet { } /// Migrates delegation of `amount` from `source` account to `destination` account. - fn migrate_delegation( + fn do_migrate_delegation( source_delegator: &T::AccountId, destination_delegator: &T::AccountId, amount: BalanceOf, diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 37b42480a0a0..0fdaf115b24e 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -606,7 +606,7 @@ mod staking_integration { // fund them with ED fund(&delegator, ExistentialDeposit::get()); // migrate 1/4th amount into each delegator - assert_ok!(DelegatedStaking::claim_delegation( + assert_ok!(DelegatedStaking::migrate_delegation( RawOrigin::Signed(200).into(), delegator, delegator_share @@ -632,7 +632,7 @@ mod staking_integration { // cannot use migrate delegator anymore assert_noop!( - DelegatedStaking::claim_delegation(RawOrigin::Signed(200).into(), 305, 1), + DelegatedStaking::migrate_delegation(RawOrigin::Signed(200).into(), 305, 1), Error::::NotEnoughFunds ); }); From 24d05adfab28b3a7584c642178f266e5a8f3265c Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 25 Apr 2024 16:12:53 +0200 Subject: [PATCH 77/85] fix docs --- substrate/frame/delegated-staking/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index 5911a2c55ccb..d4d6ef0b3652 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -72,7 +72,7 @@ //! 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::claim_delegation`]. +//! 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 @@ -307,7 +307,7 @@ pub mod pallet { /// 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::claim_delegation] to + /// 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. pub fn migrate_to_agent( origin: OriginFor, From fe0b657ed5e2aa2efd4d9f4decddfcd0c0cf913a Mon Sep 17 00:00:00 2001 From: Ankan Date: Tue, 7 May 2024 13:57:42 +0200 Subject: [PATCH 78/85] fix migration, move free funds as well when migrating a nominator to agent. --- substrate/frame/delegated-staking/src/lib.rs | 36 ++++++++++++++----- substrate/frame/delegated-staking/src/mock.rs | 2 +- .../frame/delegated-staking/src/tests.rs | 21 +++++++---- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index d4d6ef0b3652..b0b6c3d302ec 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -147,7 +147,7 @@ use frame_support::{ }, Balanced, Inspect as FunInspect, Mutate as FunMutate, }, - tokens::{fungible::Credit, Precision, Preservation}, + tokens::{fungible::Credit, Fortitude, Precision, Preservation}, Defensive, DefensiveOption, Imbalance, OnUnbalanced, }, }; @@ -301,14 +301,14 @@ pub mod pallet { /// The origin needs to /// - be a `Nominator` with [`Config::CoreStaking`], /// - not already an `Agent`, - /// - have enough funds to transfer existential deposit to a delegator account created for - /// the migration. /// /// 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, @@ -474,13 +474,33 @@ impl Pallet { // release funds from core staking. T::CoreStaking::migrate_to_virtual_staker(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::Expendable) - .map_err(|_| Error::::BadState)?; + // 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)?; - Self::do_delegate(&proxy_delegator, who, stake.total) + // 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`]. diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 6a5717533410..bba4088d7c98 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -124,7 +124,6 @@ impl pallet_staking::Config for Runtime { 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; @@ -135,6 +134,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = DelegatedStaking; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 0fdaf115b24e..86e8f95bf7b6 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -564,8 +564,10 @@ mod staking_integration { fn migration_works() { ExtBuilder::default().build_and_execute(|| { // add a nominator - fund(&200, 5000); let staked_amount = 4000; + let agent_amount = 5000; + fund(&200, agent_amount); + assert_ok!(Staking::bond( RuntimeOrigin::signed(200), staked_amount, @@ -578,7 +580,7 @@ mod staking_integration { // 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); + 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 @@ -588,19 +590,20 @@ mod staking_integration { assert_ok!(DelegatedStaking::migrate_to_agent(RawOrigin::Signed(200).into(), 201)); // verify all went well - let mut expected_proxy_delegated_amount = staked_amount; + 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), 5000 - staked_amount); + assert_eq!(Balances::free_balance(200), 0); assert_eq!(Staking::stake(&200).unwrap(), init_stake); - assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); + 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 = staked_amount / 4; + let delegator_share = agent_amount / 4; for delegator in 300..304 { assert_eq!(Balances::free_balance(delegator), 0); // fund them with ED @@ -626,8 +629,12 @@ mod staking_integration { // delegate stake is unchanged. assert_eq!(Staking::stake(&200).unwrap(), init_stake); - assert_eq!(get_agent(&200).ledger.effective_balance(), 4000); + 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 From 7782cabda9f69ff05cb398adb5c5bc18decc10e7 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 13 May 2024 13:01:35 +0200 Subject: [PATCH 79/85] pr feedbacks --- substrate/frame/delegated-staking/src/lib.rs | 9 ++-- substrate/frame/delegated-staking/src/mock.rs | 13 ------ .../frame/delegated-staking/src/tests.rs | 46 ++++++++++++++++++- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/substrate/frame/delegated-staking/src/lib.rs b/substrate/frame/delegated-staking/src/lib.rs index b0b6c3d302ec..210f69d9c839 100644 --- a/substrate/frame/delegated-staking/src/lib.rs +++ b/substrate/frame/delegated-staking/src/lib.rs @@ -125,19 +125,17 @@ #![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::*; -mod types; - use types::*; -mod impls; - use frame_support::{ pallet_prelude::*, traits::{ @@ -271,6 +269,9 @@ pub mod pallet { /// 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 diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index bba4088d7c98..7399cb7a6e9b 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -151,19 +151,6 @@ impl delegated_staking::Config for Runtime { 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; } diff --git a/substrate/frame/delegated-staking/src/tests.rs b/substrate/frame/delegated-staking/src/tests.rs index 86e8f95bf7b6..46ec3e71ed2d 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -74,7 +74,7 @@ fn cannot_become_agent() { Error::::AlreadyStaking ); - // an existing nominator cannot become agent + // an existing direct staker to `CoreStaking` cannot become an agent. assert_noop!( DelegatedStaking::register_agent( RawOrigin::Signed(mock::GENESIS_NOMINATOR_ONE).into(), @@ -193,6 +193,49 @@ fn agent_restrictions() { ), 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 + ); }); } @@ -363,7 +406,6 @@ mod staking_integration { Error::::NotEnoughFunds ); - assert!(eq_stake(agent, total_staked, expected_active)); assert_eq!(get_agent(&agent).available_to_bond(), 0); // full amount is still delegated assert_eq!(get_agent(&agent).ledger.effective_balance(), total_staked); From 6e3045fdb52406e702bdf97eabdb4fdca10bff02 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 13 May 2024 13:10:55 +0200 Subject: [PATCH 80/85] use system events for pallet --- substrate/frame/delegated-staking/src/mock.rs | 10 +--------- substrate/frame/delegated-staking/src/tests.rs | 6 +----- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 7399cb7a6e9b..1dd58a084cb3 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -32,8 +32,6 @@ use frame_election_provider_support::{ }; 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; @@ -293,13 +291,7 @@ parameter_types! { #[allow(unused)] 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 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 index 46ec3e71ed2d..1f36f655beb8 100644 --- a/substrate/frame/delegated-staking/src/tests.rs +++ b/substrate/frame/delegated-staking/src/tests.rs @@ -229,11 +229,7 @@ fn agent_restrictions() { // agent cannot delegate to self assert_noop!( - DelegatedStaking::delegate_to_agent( - RawOrigin::Signed(agent_one).into(), - agent_one, - 10 - ), + DelegatedStaking::delegate_to_agent(RawOrigin::Signed(agent_one).into(), agent_one, 10), Error::::InvalidDelegation ); }); From 159d1bfe69d96ef5b9455275a8af4fe6b62a25d8 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 13 May 2024 13:51:47 +0200 Subject: [PATCH 81/85] run try state for staking --- substrate/frame/delegated-staking/src/mock.rs | 10 ++++++++-- substrate/frame/staking/src/lib.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index 1dd58a084cb3..c58ff27743c4 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -31,7 +31,7 @@ use frame_election_provider_support::{ onchain, SequentialPhragmen, }; use frame_support::dispatch::RawOrigin; -use pallet_staking::CurrentEra; +use pallet_staking::{ActiveEra, ActiveEraInfo, CurrentEra}; use sp_staking::{Stake, StakingInterface}; pub type T = Runtime; @@ -113,7 +113,7 @@ impl pallet_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Slash = (); type Reward = (); - type SessionsPerEra = (); + type SessionsPerEra = ConstU32<1>; type SlashDeferDuration = (); type AdminOrigin = frame_system::EnsureRoot; type BondingDuration = BondingDuration; @@ -220,6 +220,8 @@ impl ExtBuilder { ext.execute_with(|| { // for events to be deposited. frame_system::Pallet::::set_block_number(1); + // set era for staking. + start_era(0); }); ext @@ -229,6 +231,9 @@ impl ExtBuilder { 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(); }); } @@ -274,6 +279,7 @@ pub(crate) fn setup_delegation_stake( 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 { 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. From 52ae957b422ba901609027ac3383a812be47f196 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 13 May 2024 13:59:23 +0200 Subject: [PATCH 82/85] fmt --- substrate/frame/delegated-staking/src/mock.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/substrate/frame/delegated-staking/src/mock.rs b/substrate/frame/delegated-staking/src/mock.rs index c58ff27743c4..21a9fe6b2270 100644 --- a/substrate/frame/delegated-staking/src/mock.rs +++ b/substrate/frame/delegated-staking/src/mock.rs @@ -113,7 +113,7 @@ impl pallet_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Slash = (); type Reward = (); - type SessionsPerEra = ConstU32<1>; + type SessionsPerEra = ConstU32<1>; type SlashDeferDuration = (); type AdminOrigin = frame_system::EnsureRoot; type BondingDuration = BondingDuration; @@ -232,7 +232,11 @@ impl ExtBuilder { ext.execute_with(test); ext.execute_with(|| { #[cfg(feature = "try-runtime")] - >::try_state(frame_system::Pallet::::block_number(), frame_support::traits::TryStateSelect::All).unwrap(); + >::try_state( + frame_system::Pallet::::block_number(), + frame_support::traits::TryStateSelect::All, + ) + .unwrap(); #[cfg(not(feature = "try-runtime"))] DelegatedStaking::do_try_state().unwrap(); }); From 78fac9ea0d4fe4b65eec17dbfa76b9f5ad558055 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 15 May 2024 11:31:34 +0200 Subject: [PATCH 83/85] initial release version for delegate staking pallet --- Cargo.lock | 2 +- substrate/frame/delegated-staking/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b5686b04246..a090f2aaa6be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10126,7 +10126,7 @@ dependencies = [ [[package]] name = "pallet-delegated-staking" -version = "4.0.0-dev" +version = "1.0.0" dependencies = [ "frame-election-provider-support", "frame-support", diff --git a/substrate/frame/delegated-staking/Cargo.toml b/substrate/frame/delegated-staking/Cargo.toml index cecaefbe779f..a9cbd758ed09 100644 --- a/substrate/frame/delegated-staking/Cargo.toml +++ b/substrate/frame/delegated-staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-delegated-staking" -version = "4.0.0-dev" +version = "1.0.0" authors.workspace = true edition.workspace = true license = "Apache-2.0" From 3264782aeb77388ccf6e586afdef26b0d8b4fb61 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 15 May 2024 12:33:05 +0200 Subject: [PATCH 84/85] initial version set to 1.0.0 and bump as patch --- prdoc/pr_3904.prdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prdoc/pr_3904.prdoc b/prdoc/pr_3904.prdoc index edfcddca8462..82d383782fe0 100644 --- a/prdoc/pr_3904.prdoc +++ b/prdoc/pr_3904.prdoc @@ -12,4 +12,4 @@ doc: crates: - name: pallet-delegated-staking - + bump: patch From cd5eb94bcbd45828d039ef51b671e7e823827204 Mon Sep 17 00:00:00 2001 From: Ankan Date: Wed, 15 May 2024 12:57:28 +0200 Subject: [PATCH 85/85] add other pallets affected in prdoc --- prdoc/pr_3904.prdoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/prdoc/pr_3904.prdoc b/prdoc/pr_3904.prdoc index 82d383782fe0..694f9b443877 100644 --- a/prdoc/pr_3904.prdoc +++ b/prdoc/pr_3904.prdoc @@ -13,3 +13,7 @@ doc: crates: - name: pallet-delegated-staking bump: patch + - name: pallet-staking + bump: patch + - name: sp-staking + bump: minor