From bb81154c666cf7d0956785d7fb77394943c7105b Mon Sep 17 00:00:00 2001 From: NZT48 Date: Sun, 22 Dec 2024 22:04:42 +0100 Subject: [PATCH 1/6] Add Zombienet config --- scripts/zombienet/local_neuroweb.toml | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 scripts/zombienet/local_neuroweb.toml diff --git a/scripts/zombienet/local_neuroweb.toml b/scripts/zombienet/local_neuroweb.toml new file mode 100644 index 0000000..9b77a99 --- /dev/null +++ b/scripts/zombienet/local_neuroweb.toml @@ -0,0 +1,44 @@ +[settings] +timeout = 180000 +node_verifier = "None" + +[relaychain] +chain = "rococo-local" +default_command = "./polkadot" +args = [ "--no-hardware-benchmarks --offchain-worker always --workers-path ./"] + + [[relaychain.nodes]] + name = "alice" + validator = true + + [[relaychain.nodes]] + name = "bob" + validator = true + + [[relaychain.nodes]] + name = "charlie" + validator = true + + [[relaychain.nodes]] + name = "dave" + validator = true + +[[parachains]] +id = 2043 +addToGenesis = true +cumulus_based = true +chain = "dev" + + [[parachains.collators]] + name = "neuroweb-collator01" + command = "./neuroweb/target/debug/neuroweb" + args = [ "--no-hardware-benchmarks"] + ws_port = 9933 + rpc_port = 8833 + + [[parachains.collators]] + name = "neuroweb-collator02" + ws_port = 9944 + rpc_port = 8822 + command = "./neuroweb/target/debug/neuroweb" + args = [ "--no-hardware-benchmarks"] From e2b743a248db50d6a2c749de49858ff6dc75a465 Mon Sep 17 00:00:00 2001 From: NZT48 Date: Sun, 22 Dec 2024 22:05:00 +0100 Subject: [PATCH 2/6] Add parachain staking pallet implementation --- pallets/parachain-staking/Cargo.toml | 79 + pallets/parachain-staking/README.md | 151 + .../parachain-staking/src/auto_compound.rs | 338 + pallets/parachain-staking/src/benchmarks.rs | 1482 +++++ .../src/delegation_requests.rs | 576 ++ pallets/parachain-staking/src/inflation.rs | 172 + pallets/parachain-staking/src/lib.rs | 1950 ++++++ pallets/parachain-staking/src/migrations.rs | 161 + pallets/parachain-staking/src/mock.rs | 928 +++ pallets/parachain-staking/src/set.rs | 85 + pallets/parachain-staking/src/tests.rs | 5448 +++++++++++++++++ pallets/parachain-staking/src/traits.rs | 55 + pallets/parachain-staking/src/types.rs | 1600 +++++ pallets/parachain-staking/src/weights.rs | 1455 +++++ 14 files changed, 14480 insertions(+) create mode 100644 pallets/parachain-staking/Cargo.toml create mode 100644 pallets/parachain-staking/README.md create mode 100644 pallets/parachain-staking/src/auto_compound.rs create mode 100644 pallets/parachain-staking/src/benchmarks.rs create mode 100644 pallets/parachain-staking/src/delegation_requests.rs create mode 100644 pallets/parachain-staking/src/inflation.rs create mode 100644 pallets/parachain-staking/src/lib.rs create mode 100644 pallets/parachain-staking/src/migrations.rs create mode 100644 pallets/parachain-staking/src/mock.rs create mode 100644 pallets/parachain-staking/src/set.rs create mode 100644 pallets/parachain-staking/src/tests.rs create mode 100644 pallets/parachain-staking/src/traits.rs create mode 100644 pallets/parachain-staking/src/types.rs create mode 100644 pallets/parachain-staking/src/weights.rs diff --git a/pallets/parachain-staking/Cargo.toml b/pallets/parachain-staking/Cargo.toml new file mode 100644 index 0000000..c1ffefc --- /dev/null +++ b/pallets/parachain-staking/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "pallet-parachain-staking" +description = "Parachain staking pallet for collator selection and reward distribution" +authors = { workspace = true } +edition = "2021" +version = "1.0.0" + +[dependencies] +serde = { workspace = true } +log = { workspace = true } + +codec = { workspace = true, features = [ + "derive", +] } +scale-info = { workspace = true, features = ["derive"] } + +# FRAME +frame-benchmarking = { workspace = true, optional = true } +frame-support.workspace = true +frame-system.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +sp-core.workspace = true +substrate-fixed.workspace = true +sp-staking.workspace = true +pallet-authorship.workspace = true +pallet-session.workspace = true + +[dev-dependencies] +pallet-balances.workspace = true +pallet-aura.workspace = true +pallet-timestamp.workspace = true +similar-asserts = "1.1.0" +sp-io.workspace = true +sp-consensus-aura.workspace = true + +[features] +default = [ "std" ] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-aura/std", + "pallet-authorship/std", + "pallet-balances/std", + "pallet-session/std", + "pallet-timestamp/std", + "codec/std", + "scale-info/std", + "serde/std", + "sp-consensus-aura/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", + "substrate-fixed/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-benchmarking?/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-staking/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-aura/try-runtime", + "pallet-authorship/try-runtime", + "pallet-balances/try-runtime", + "pallet-session/try-runtime", + "pallet-timestamp/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/parachain-staking/README.md b/pallets/parachain-staking/README.md new file mode 100644 index 0000000..a159567 --- /dev/null +++ b/pallets/parachain-staking/README.md @@ -0,0 +1,151 @@ +# DPoS Pallet for Parachain Staking + +The parachain-staking pallet provides minimal staking functionality, enabling direct delegation for collator selection based on total backed stake. Unlike frame/pallet-staking, this pallet does not use Phragmen for delegation but allows delegators to choose exactly who to back. This repository contains a modified version of Moonbeam's and Polimec's 'parachain_staking' Substrate Pallet. + +## General Rules + +### Rounds + +* A new round starts every >::get().length blocks. +* At the start of each round: + * Rewards for block authoring from T::RewardPaymentDelay rounds ago are calculated. + * A new set of collators is selected from the candidates. + +### Joining and Leaving + +* Collators + * To join as a collator candidate, an account must bond at least MinCandidateStk. + * To leave, the account must call schedule_leave_candidates, and the exit is executable after T::LeaveCandidatesDelay rounds. + +* Delegators + * To delegate, an account must bond at least MinDelegatorStk. + * Delegators can revoke a delegation or leave entirely by calling the relevant extrinsics. + +### Reward Distribution + +* Rewards are distributed with a delay of T::RewardPaymentDelay rounds. +* Collators and their top T::MaxTopDelegationsPerCandidate delegators receive rewards proportionally based on their stake. + +### Selection + +* Each round, a fixed number (T::TotalSelected) of collators are chosen based on their total backing stake (self-bond + delegations). + +## Extrinsics and Parameters + +### General Extrinsics + +* set_staking_expectations(expectations: Range>) + * Purpose: Sets the expected total stake range for collators and delegators. + * Parameters: + * expectations: Defines minimum, ideal, and maximum total stake values. + +* set_inflation(schedule: Range) + * Purpose: Configures the annual inflation rate to determine per-round issuance. + * Parameters: + * schedule: Defines minimum, ideal, and maximum inflation rates. + +* set_blocks_per_round(new: u32) + * Purpose: Sets the number of blocks per round. + * Parameters: + * new: The new block length for a round. + +### Collator-Specific Extrinsics + +* join_candidates(bond: BalanceOf, candidate_count: u32) + * Purpose: Allows an account to become a collator candidate. + * Parameters: + * bond: The amount of funds to bond as a self-stake. + * candidate_count: The current number of candidates in the pool (used as a weight hint). + +* schedule_leave_candidates(candidate_count: u32) + * Purpose: Requests to leave the set of collators. + * Parameters: + * candidate_count: The current number of candidates in the pool (used as a weight hint). + +* execute_leave_candidates(candidate: T::AccountId, candidate_delegation_count: u32) + * Purpose: Executes the leave request for a collator candidate. + * Parameters: + * candidate: The account of the collator. + * candidate_delegation_count: The number of delegators backing the collator (used as a weight hint). + +* candidate_bond_more(more: BalanceOf) + * Purpose: Increases the self-bond of a collator candidate. + * Parameters: + * more: The additional amount to bond. + +* schedule_candidate_bond_less(less: BalanceOf) + * Purpose: Schedules a decrease in the self-bond of a collator candidate. + * Parameters: + * less: The amount to decrease. + +* execute_candidate_bond_less(candidate: T::AccountId) + * Purpose: Executes a scheduled decrease in the self-bond of a collator. + * Parameters: + * candidate: The account of the collator. + +### Delegator-Specific Extrinsics + +* delegate(candidate: T::AccountId, amount: BalanceOf, candidate_delegation_count: u32, delegation_count: u32) + * Purpose: Allows an account to delegate stake to a collator candidate. + * Parameters: + * candidate: The account of the collator to delegate to. + * amount: The amount of funds to delegate. + * candidate_delegation_count: The number of delegators already backing the collator (used as a weight hint). + * delegation_count: The number of delegations by the delegator (used as a weight hint). + +* schedule_revoke_delegation(collator: T::AccountId) + * Purpose: Schedules the revocation of a delegation to a collator. + * Parameters: + * collator: The account of the collator. + +* execute_delegation_request(delegator: T::AccountId, candidate: T::AccountId) + * Purpose: Executes a scheduled delegation revocation or decrease. + * Parameters: + * delegator: The account of the delegator. + * candidate: The account of the collator. + +* delegator_bond_more(candidate: T::AccountId, more: BalanceOf) + * Purpose: Increases the bond of a delegator to a specific collator. + * Parameters: + * candidate: The account of the collator. + * more: The additional amount to bond. + +* schedule_delegator_bond_less(candidate: T::AccountId, less: BalanceOf) + * Purpose: Schedules a decrease in the bond of a delegator to a specific collator. + * Parameters: + * candidate: The account of the collator. + * less: The amount to decrease. + +* schedule_leave_delegators() + * Purpose: Schedules the exit of a delegator from all delegations. + +* execute_leave_delegators(delegator: T::AccountId, delegation_count: u32) + * Purpose: Executes the exit of a delegator from all delegations. + * Parameters: + * delegator: The account of the delegator. + * delegation_count: The number of delegations by the delegator (used as a weight hint). + + +### Storage Items + +* CandidateInfo: Stores metadata for collator candidates. +* DelegatorState: Stores the state of delegators. +* AtStake: Snapshots collator delegation stake at the start of each round. +* DelayedPayouts: Stores delayed payout information for rewards. +* Points: Tracks points awarded to collators for block production. + +### Events + +* NewRound: Emitted when a new round begins. +* Rewarded: Emitted when a reward is paid to an account. +* CollatorChosen: Emitted when a collator is selected for the next round. +* Delegation: Emitted when a new delegation is made. + +## Modifications +The modifications to the original pallet include the following: +1. Removed Nimbus Dependencies: The original dependencies on Nimbus have been removed. This simplifies the usage of the pallet and makes it independent of Nimbus. +2. Implemented Traits from **pallet_authorship** and **pallet_session**: To replace some functionality previously provided by Nimbus, several traits from _pallet_authorship_ and _pallet_session_ have been implemented: + - **EventHandler** from *pallet_authorship*: This trait is used to note the block author and award them points for producing a block. The points are then used for staking purposes.q + - **SessionManager** from *pallet_session*: This trait is used to manage the start and end of sessions, as well as assemble new collators for new sessions. + - **ShouldEndSession** from *pallet_session*: This trait is used to decide when a session should end. + - **EstimateNextSessionRotation** from *pallet_session*: This trait is used to estimate the average session length and the current session progress, as well as estimate the next session rotation. \ No newline at end of file diff --git a/pallets/parachain-staking/src/auto_compound.rs b/pallets/parachain-staking/src/auto_compound.rs new file mode 100644 index 0000000..e1f2911 --- /dev/null +++ b/pallets/parachain-staking/src/auto_compound.rs @@ -0,0 +1,338 @@ +// 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 . + +//! Auto-compounding functionality for staking rewards + +use crate::{ + pallet::{ + AddGet, AutoCompoundingDelegations as AutoCompoundingDelegationsStorage, BalanceOf, CandidateInfo, Config, + DelegatorState, Error, Event, Pallet, Total, + }, + types::{Bond, BondAdjust, Delegator}, +}; +use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, traits::Get}; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::{traits::Saturating, BoundedVec, Percent, RuntimeDebug}; +use sp_std::prelude::*; + +/// Represents the auto-compounding amount for a delegation. +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)] +pub struct AutoCompoundConfig { + pub delegator: AccountId, + pub value: Percent, +} + +/// Represents the auto-compounding [Delegations] for `T: Config` +#[derive(Clone, Eq, PartialEq, RuntimeDebug)] +pub struct AutoCompoundDelegations( + BoundedVec< + AutoCompoundConfig, + AddGet, + >, +); + +impl AutoCompoundDelegations +where + T: Config, +{ + /// Creates a new instance of [AutoCompoundingDelegations] from a vector of sorted_delegations. + /// This is used for testing purposes only. + #[cfg(test)] + pub fn new( + sorted_delegations: BoundedVec< + AutoCompoundConfig, + AddGet, + >, + ) -> Self { + Self(sorted_delegations) + } + + /// Retrieves an instance of [AutoCompoundingDelegations] storage as [AutoCompoundDelegations]. + pub fn get_storage(candidate: &T::AccountId) -> Self { + Self(>::get(candidate)) + } + + /// Inserts the current state to [AutoCompoundingDelegations] storage. + pub fn set_storage(self, candidate: &T::AccountId) { + >::insert(candidate, self.0) + } + + /// Retrieves the auto-compounding value for a delegation. The `delegations_config` must be a + /// sorted vector for binary_search to work. + pub fn get_for_delegator(&self, delegator: &T::AccountId) -> Option { + match self.0.binary_search_by(|d| d.delegator.cmp(delegator)) { + Ok(index) => Some(self.0[index].value), + Err(_) => None, + } + } + + /// Sets the auto-compounding value for a delegation. The `delegations_config` must be a sorted + /// vector for binary_search to work. + pub fn set_for_delegator(&mut self, delegator: T::AccountId, value: Percent) -> Result> { + match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) { + Ok(index) => + if self.0[index].value == value { + Ok(false) + } else { + self.0[index].value = value; + Ok(true) + }, + Err(index) => { + self.0 + .try_insert(index, AutoCompoundConfig { delegator, value }) + .map_err(|_| Error::::ExceedMaxDelegationsPerDelegator)?; + Ok(true) + }, + } + } + + /// Removes the auto-compounding value for a delegation. + /// Returns `true` if the entry was removed, `false` otherwise. The `delegations_config` must be a + /// sorted vector for binary_search to work. + pub fn remove_for_delegator(&mut self, delegator: &T::AccountId) -> bool { + match self.0.binary_search_by(|d| d.delegator.cmp(delegator)) { + Ok(index) => { + self.0.remove(index); + true + }, + Err(_) => false, + } + } + + /// Returns the length of the inner vector. + pub fn len(&self) -> u32 { + self.0.len() as u32 + } + + /// Returns a reference to the inner vector. + #[cfg(test)] + pub fn inner( + &self, + ) -> &BoundedVec< + AutoCompoundConfig, + AddGet, + > { + &self.0 + } + + /// Converts the [AutoCompoundDelegations] into the inner vector. + #[cfg(test)] + pub fn into_inner( + self, + ) -> BoundedVec< + AutoCompoundConfig, + AddGet, + > { + self.0 + } + + // -- pallet functions -- + + /// Delegates and sets the auto-compounding config. The function skips inserting auto-compound + /// storage and validation, if the auto-compound value is 0%. + pub(crate) fn delegate_with_auto_compound( + candidate: T::AccountId, + delegator: T::AccountId, + amount: BalanceOf, + auto_compound: Percent, + candidate_delegation_count_hint: u32, + candidate_auto_compounding_delegation_count_hint: u32, + delegation_count_hint: u32, + ) -> DispatchResultWithPostInfo { + // check that caller can lock the amount before any changes to storage + ensure!( + >::get_delegator_stakable_free_balance(&delegator) >= amount, + Error::::InsufficientBalance + ); + + let mut delegator_state = if let Some(mut state) = >::get(&delegator) { + // delegation after first + ensure!(amount >= T::MinDelegation::get(), Error::::DelegationBelowMin); + ensure!( + delegation_count_hint >= state.delegations.0.len() as u32, + Error::::TooLowDelegationCountToDelegate + ); + ensure!( + (state.delegations.0.len() as u32) < T::MaxDelegationsPerDelegator::get(), + Error::::ExceedMaxDelegationsPerDelegator + ); + ensure!( + state.add_delegation(Bond { owner: candidate.clone(), amount }), + Error::::AlreadyDelegatedCandidate + ); + state + } else { + // first delegation + ensure!(amount >= T::MinDelegatorStk::get(), Error::::DelegatorBondBelowMin); + ensure!(!>::is_candidate(&delegator), Error::::CandidateExists); + Delegator::new(delegator.clone(), candidate.clone(), amount) + }; + let mut candidate_state = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + ensure!( + candidate_delegation_count_hint >= candidate_state.delegation_count, + Error::::TooLowCandidateDelegationCountToDelegate + ); + + let auto_compounding_state = if !auto_compound.is_zero() { + let auto_compounding_state = Self::get_storage(&candidate); + ensure!( + auto_compounding_state.len() <= candidate_auto_compounding_delegation_count_hint, + >::TooLowCandidateAutoCompoundingDelegationCountToDelegate, + ); + Some(auto_compounding_state) + } else { + None + }; + + // add delegation to candidate + let (delegator_position, less_total_staked) = + candidate_state.add_delegation::(&candidate, Bond { owner: delegator.clone(), amount })?; + + // lock delegator amount + delegator_state.adjust_bond_lock::(BondAdjust::Increase(amount))?; + + // adjust total locked, + // only is_some if kicked the lowest bottom as a consequence of this new delegation + let net_total_increase = if let Some(less) = less_total_staked { amount.saturating_sub(less) } else { amount }; + let new_total_locked = >::get().saturating_add(net_total_increase); + + // maybe set auto-compound config, state is Some if the percent is non-zero + if let Some(mut state) = auto_compounding_state { + state.set_for_delegator(delegator.clone(), auto_compound)?; + state.set_storage(&candidate); + } + + >::put(new_total_locked); + >::insert(&candidate, candidate_state); + >::insert(&delegator, delegator_state); + >::deposit_event(Event::Delegation { + delegator, + locked_amount: amount, + candidate, + delegator_position, + auto_compound, + }); + + Ok(().into()) + } + + /// Sets the auto-compounding value for a delegation. The config is removed if value is zero. + pub(crate) fn set_auto_compound( + candidate: T::AccountId, + delegator: T::AccountId, + value: Percent, + candidate_auto_compounding_delegation_count_hint: u32, + delegation_count_hint: u32, + ) -> DispatchResultWithPostInfo { + let delegator_state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + ensure!( + delegator_state.delegations.0.len() <= delegation_count_hint as usize, + >::TooLowDelegationCountToAutoCompound, + ); + ensure!(delegator_state.delegations.0.iter().any(|b| b.owner == candidate), >::DelegationDNE,); + + let mut auto_compounding_state = Self::get_storage(&candidate); + ensure!( + auto_compounding_state.len() <= candidate_auto_compounding_delegation_count_hint, + >::TooLowCandidateAutoCompoundingDelegationCountToAutoCompound, + ); + let state_updated = if value.is_zero() { + auto_compounding_state.remove_for_delegator(&delegator) + } else { + auto_compounding_state.set_for_delegator(delegator.clone(), value)? + }; + if state_updated { + auto_compounding_state.set_storage(&candidate); + } + + >::deposit_event(Event::AutoCompoundSet { candidate, delegator, value }); + + Ok(().into()) + } + + /// Removes the auto-compounding value for a delegation. This should be called when the + /// delegation is revoked to cleanup storage. Storage is only written iff the entry existed. + pub(crate) fn remove_auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) { + let mut auto_compounding_state = Self::get_storage(candidate); + if auto_compounding_state.remove_for_delegator(delegator) { + auto_compounding_state.set_storage(candidate); + } + } + + /// Returns the value of auto-compound, if it exists for a given delegation, zero otherwise. + pub(crate) fn auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) -> Percent { + let delegations_config = Self::get_storage(candidate); + delegations_config.get_for_delegator(delegator).unwrap_or_else(Percent::zero) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::Test; + + #[test] + fn test_set_for_delegator_inserts_config_and_returns_true_if_entry_missing() { + let mut delegations_config = AutoCompoundDelegations::::new(vec![].try_into().expect("must succeed")); + assert_eq!(true, delegations_config.set_for_delegator(1, Percent::from_percent(50)).expect("must succeed")); + assert_eq!( + vec![AutoCompoundConfig { delegator: 1, value: Percent::from_percent(50) }], + delegations_config.into_inner().into_inner(), + ); + } + + #[test] + fn test_set_for_delegator_updates_config_and_returns_true_if_entry_changed() { + let mut delegations_config = AutoCompoundDelegations::::new( + vec![AutoCompoundConfig { delegator: 1, value: Percent::from_percent(10) }] + .try_into() + .expect("must succeed"), + ); + assert_eq!(true, delegations_config.set_for_delegator(1, Percent::from_percent(50)).expect("must succeed")); + assert_eq!( + vec![AutoCompoundConfig { delegator: 1, value: Percent::from_percent(50) }], + delegations_config.into_inner().into_inner(), + ); + } + + #[test] + fn test_set_for_delegator_updates_config_and_returns_false_if_entry_unchanged() { + let mut delegations_config = AutoCompoundDelegations::::new( + vec![AutoCompoundConfig { delegator: 1, value: Percent::from_percent(10) }] + .try_into() + .expect("must succeed"), + ); + assert_eq!(false, delegations_config.set_for_delegator(1, Percent::from_percent(10)).expect("must succeed")); + assert_eq!( + vec![AutoCompoundConfig { delegator: 1, value: Percent::from_percent(10) }], + delegations_config.into_inner().into_inner(), + ); + } + + #[test] + fn test_remove_for_delegator_returns_false_if_entry_was_missing() { + let mut delegations_config = AutoCompoundDelegations::::new(vec![].try_into().expect("must succeed")); + assert_eq!(false, delegations_config.remove_for_delegator(&1),); + } + + #[test] + fn test_remove_delegation_config_returns_true_if_entry_existed() { + let mut delegations_config = AutoCompoundDelegations::::new( + vec![AutoCompoundConfig { delegator: 1, value: Percent::from_percent(10) }] + .try_into() + .expect("must succeed"), + ); + assert_eq!(true, delegations_config.remove_for_delegator(&1)); + } +} diff --git a/pallets/parachain-staking/src/benchmarks.rs b/pallets/parachain-staking/src/benchmarks.rs new file mode 100644 index 0000000..6554002 --- /dev/null +++ b/pallets/parachain-staking/src/benchmarks.rs @@ -0,0 +1,1482 @@ +// 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 . + +#![cfg(feature = "runtime-benchmarks")] + +//! Benchmarking +use crate::{ + AwardedPts, BalanceOf, Call, CandidateBondLessRequest, Config, DelegationAction, Pallet, ParachainBondConfig, + ParachainBondInfo, Points, Range, RewardPayment, Round, ScheduledRequest, Staked, TopDelegations, +}; +use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite}; +use frame_support::traits::{ + fungible::{Inspect, Mutate}, + Get, OnFinalize, OnInitialize, +}; +use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; +#[cfg(test)] +use sp_runtime::BuildStorage; +use sp_runtime::{Perbill, Percent}; +use sp_std::{vec, vec::Vec}; + +/// Minimum collator candidate stake +fn min_candidate_stk() -> BalanceOf { + <::MinCandidateStk as Get>>::get() +} + +/// Minimum delegator stake +fn min_delegator_stk() -> BalanceOf { + <::MinDelegatorStk as Get>>::get() +} + +/// Create a funded user. +/// Extra + min_candidate_stk is total minted funds +/// Returns tuple (id, balance) +fn create_funded_user(string: &'static str, n: u32, extra: BalanceOf) -> (T::AccountId, BalanceOf) { + const SEED: u32 = 0; + let user = account(string, n, SEED); + let min_candidate_stk = min_candidate_stk::(); + let total = min_candidate_stk + extra; + let existential_deposit: BalanceOf = T::Currency::minimum_balance(); + T::Currency::set_balance(&user, total + existential_deposit); + (user, total) +} + +/// Create a funded delegator. +fn create_funded_delegator( + string: &'static str, + n: u32, + extra: BalanceOf, + collator: T::AccountId, + min_bond: bool, + collator_delegator_count: u32, +) -> Result { + let (user, total) = create_funded_user::(string, n, extra); + let bond = if min_bond { min_delegator_stk::() } else { total }; + Pallet::::delegate( + RawOrigin::Signed(user.clone()).into(), + collator, + bond, + collator_delegator_count, + 0u32, // first delegation for all calls + )?; + Ok(user) +} + +/// Create a funded collator. +fn create_funded_collator( + string: &'static str, + n: u32, + extra: BalanceOf, + min_bond: bool, + candidate_count: u32, +) -> Result { + let (user, total) = create_funded_user::(string, n, extra); + let bond = if min_bond { min_candidate_stk::() } else { total }; + Pallet::::join_candidates(RawOrigin::Signed(user.clone()).into(), bond, candidate_count)?; + Ok(user) +} + +// Simulate staking on finalize by manually setting points +fn parachain_staking_on_finalize(author: T::AccountId) { + let now = >::get().current; + let score_plus_20 = >::get(now, &author).saturating_add(20); + >::insert(now, author, score_plus_20); + >::mutate(now, |x| *x = x.saturating_add(20)); +} + +/// Run to end block and author +fn roll_to_and_author(round_delay: u32, author: T::AccountId) { + let total_rounds = round_delay + 1u32; + let round_length: BlockNumberFor = Pallet::::round().length.into(); + let mut now = >::block_number() + 1u32.into(); + let end = Pallet::::round().first + (round_length * total_rounds.into()); + while now < end { + parachain_staking_on_finalize::(author.clone()); + >::on_finalize(>::block_number()); + >::set_block_number(>::block_number() + 1u32.into()); + >::on_initialize(>::block_number()); + Pallet::::on_initialize(>::block_number()); + now += 1u32.into(); + } +} + +const USER_SEED: u32 = 999666; +struct Seed { + pub inner: u32, +} +impl Seed { + fn new() -> Self { + Seed { inner: USER_SEED } + } + + pub fn take(&mut self) -> u32 { + let v = self.inner; + self.inner += 1; + v + } +} + +benchmarks! { + // MONETARY ORIGIN DISPATCHABLES + set_staking_expectations { + let stake_range: Range> = Range { + min: 100u32.into(), + ideal: 200u32.into(), + max: 300u32.into(), + }; + }: _(RawOrigin::Root, stake_range) + verify { + assert_eq!(Pallet::::inflation_config().expect, stake_range); + } + + set_inflation { + let inflation_range: Range = Range { + min: Perbill::from_perthousand(1), + ideal: Perbill::from_perthousand(2), + max: Perbill::from_perthousand(3), + }; + + }: _(RawOrigin::Root, inflation_range) + verify { + assert_eq!(Pallet::::inflation_config().annual, inflation_range); + } + + set_parachain_bond_account { + let parachain_bond_account: T::AccountId = account("TEST", 0u32, USER_SEED); + }: _(RawOrigin::Root, parachain_bond_account.clone()) + verify { + assert_eq!(Pallet::::parachain_bond_info().account, parachain_bond_account); + } + + set_parachain_bond_reserve_percent { + }: _(RawOrigin::Root, Percent::from_percent(33)) + verify { + assert_eq!(Pallet::::parachain_bond_info().percent, Percent::from_percent(33)); + } + + // ROOT DISPATCHABLES + + set_total_selected { + Pallet::::set_blocks_per_round(RawOrigin::Root.into(), 101u32)?; + }: _(RawOrigin::Root, 100u32) + verify { + assert_eq!(Pallet::::total_selected(), 100u32); + } + + set_collator_commission {}: _(RawOrigin::Root, Perbill::from_percent(33)) + verify { + assert_eq!(Pallet::::collator_commission(), Perbill::from_percent(33)); + } + + set_blocks_per_round {}: _(RawOrigin::Root, 1200u32) + verify { + assert_eq!(Pallet::::round().length, 1200u32); + } + + // USER DISPATCHABLES + + join_candidates { + let x in 3..1_000; + // Worst Case Complexity is insertion into an ordered list so \exists full list before call + let mut candidate_count = 1u32; + for i in 2..x { + let seed = USER_SEED - i; + let collator = create_funded_collator::( + "collator", + seed, + 0u32.into(), + true, + candidate_count + )?; + candidate_count += 1u32; + } + let (caller, min_candidate_stk) = create_funded_user::("caller", USER_SEED, 0u32.into()); + }: _(RawOrigin::Signed(caller.clone()), min_candidate_stk, candidate_count) + verify { + assert!(Pallet::::is_candidate(&caller)); + } + + // This call schedules the collator's exit and removes them from the candidate pool + // -> it retains the self-bond and delegator bonds + schedule_leave_candidates { + let x in 3..1_000; + // Worst Case Complexity is removal from an ordered list so \exists full list before call + let mut candidate_count = 1u32; + for i in 2..x { + let seed = USER_SEED - i; + let collator = create_funded_collator::( + "collator", + seed, + 0u32.into(), + true, + candidate_count + )?; + candidate_count += 1u32; + } + let caller: T::AccountId = create_funded_collator::( + "caller", + USER_SEED, + 0u32.into(), + true, + candidate_count, + )?; + candidate_count += 1u32; + }: _(RawOrigin::Signed(caller.clone()), candidate_count) + verify { + assert!(Pallet::::candidate_info(&caller).expect("must exist").is_leaving()); + } + + execute_leave_candidates { + // x is total number of delegations for the candidate + let x in 2..(<::MaxTopDelegationsPerCandidate as Get>::get() + + <::MaxBottomDelegationsPerCandidate as Get>::get()); + let candidate: T::AccountId = create_funded_collator::( + "unique_caller", + USER_SEED - 100, + 0u32.into(), + true, + 1u32, + )?; + // 2nd delegation required for all delegators to ensure DelegatorState updated not removed + let second_candidate: T::AccountId = create_funded_collator::( + "unique__caller", + USER_SEED - 99, + 0u32.into(), + true, + 2u32, + )?; + let mut delegators: Vec = Vec::new(); + let mut col_del_count = 0u32; + for i in 1..x { + let seed = USER_SEED + i; + let delegator = create_funded_delegator::( + "delegator", + seed, + min_delegator_stk::(), + candidate.clone(), + true, + col_del_count, + )?; + Pallet::::delegate( + RawOrigin::Signed(delegator.clone()).into(), + second_candidate.clone(), + min_delegator_stk::(), + col_del_count, + 1u32, + )?; + Pallet::::schedule_revoke_delegation( + RawOrigin::Signed(delegator.clone()).into(), + candidate.clone() + )?; + delegators.push(delegator); + col_del_count += 1u32; + } + Pallet::::schedule_leave_candidates( + RawOrigin::Signed(candidate.clone()).into(), + 3u32 + )?; + let candidate_leave_delay = T::LeaveCandidatesDelay::get(); + roll_to_and_author::(candidate_leave_delay, candidate.clone()); + }: _(RawOrigin::Signed(candidate.clone()), candidate.clone(), col_del_count) + verify { + assert!(Pallet::::candidate_info(&candidate).is_none()); + assert!(Pallet::::candidate_info(&second_candidate).is_some()); + for delegator in delegators { + assert!(Pallet::::is_delegator(&delegator)); + } + } + + cancel_leave_candidates { + let x in 3..1_000; + // Worst Case Complexity is removal from an ordered list so \exists full list before call + let mut candidate_count = 1u32; + for i in 2..x { + let seed = USER_SEED - i; + let collator = create_funded_collator::( + "collator", + seed, + 0u32.into(), + true, + candidate_count + )?; + candidate_count += 1u32; + } + let caller: T::AccountId = create_funded_collator::( + "caller", + USER_SEED, + 0u32.into(), + true, + candidate_count, + )?; + candidate_count += 1u32; + Pallet::::schedule_leave_candidates( + RawOrigin::Signed(caller.clone()).into(), + candidate_count + )?; + candidate_count -= 1u32; + }: _(RawOrigin::Signed(caller.clone()), candidate_count) + verify { + assert!(Pallet::::candidate_info(&caller).expect("must exist").is_active()); + } + + go_offline { + let caller: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + }: _(RawOrigin::Signed(caller.clone())) + verify { + assert!(!Pallet::::candidate_info(&caller).expect("must exist").is_active()); + } + + go_online { + let caller: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + Pallet::::go_offline(RawOrigin::Signed(caller.clone()).into())?; + }: _(RawOrigin::Signed(caller.clone())) + verify { + assert!(Pallet::::candidate_info(&caller).expect("must exist").is_active()); + } + + candidate_bond_more { + let more = min_candidate_stk::(); + let caller: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + more, + true, + 1u32, + )?; + }: _(RawOrigin::Signed(caller.clone()), more) + verify { + let expected_bond = more * 2u32.into(); + assert_eq!( + Pallet::::candidate_info(&caller).expect("candidate was created, qed").bond, + expected_bond, + ); + } + + schedule_candidate_bond_less { + let min_candidate_stk = min_candidate_stk::(); + let caller: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + min_candidate_stk, + false, + 1u32, + )?; + }: _(RawOrigin::Signed(caller.clone()), min_candidate_stk) + verify { + let state = Pallet::::candidate_info(&caller).expect("request bonded less so exists"); + assert_eq!( + state.request, + Some(CandidateBondLessRequest { + amount: min_candidate_stk, + when_executable: T::CandidateBondLessDelay::get() + 1, + }) + ); + } + + execute_candidate_bond_less { + let min_candidate_stk = min_candidate_stk::(); + let caller: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + min_candidate_stk, + false, + 1u32, + )?; + Pallet::::schedule_candidate_bond_less( + RawOrigin::Signed(caller.clone()).into(), + min_candidate_stk + )?; + roll_to_and_author::(T::CandidateBondLessDelay::get(), caller.clone()); + }: { + Pallet::::execute_candidate_bond_less( + RawOrigin::Signed(caller.clone()).into(), + caller.clone() + )?; + } verify { + assert_eq!( + Pallet::::candidate_info(&caller).expect("candidate was created, qed").bond, + min_candidate_stk, + ); + } + + cancel_candidate_bond_less { + let min_candidate_stk = min_candidate_stk::(); + let caller: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + min_candidate_stk, + false, + 1u32, + )?; + Pallet::::schedule_candidate_bond_less( + RawOrigin::Signed(caller.clone()).into(), + min_candidate_stk + )?; + }: { + Pallet::::cancel_candidate_bond_less( + RawOrigin::Signed(caller.clone()).into(), + )?; + } verify { + assert!( + Pallet::::candidate_info(&caller).expect("must exist").request.is_none() + ); + } + + delegate { + let x in 3..<::MaxDelegationsPerDelegator as Get>::get(); + let y in 2..<::MaxTopDelegationsPerCandidate as Get>::get(); + // Worst Case is full of delegations before calling `delegate` + let mut collators: Vec = Vec::new(); + // Initialize MaxDelegationsPerDelegator collator candidates + for i in 2..x { + let seed = USER_SEED - i; + let collator = create_funded_collator::( + "collator", + seed, + 0u32.into(), + true, + collators.len() as u32 + 1u32, + )?; + collators.push(collator.clone()); + } + let bond = <::MinDelegatorStk as Get>>::get(); + let extra = if (bond * (collators.len() as u32 + 1u32).into()) > min_candidate_stk::() { + (bond * (collators.len() as u32 + 1u32).into()) - min_candidate_stk::() + } else { + 0u32.into() + }; + let (caller, _) = create_funded_user::("caller", USER_SEED, extra.into()); + // Delegation count + let mut del_del_count = 0u32; + // Nominate MaxDelegationsPerDelegators collator candidates + for col in collators.clone() { + Pallet::::delegate( + RawOrigin::Signed(caller.clone()).into(), col, bond, 0u32, del_del_count + )?; + del_del_count += 1u32; + } + // Last collator to be delegated + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + collators.len() as u32 + 1u32, + )?; + // Worst Case Complexity is insertion into an almost full collator + let mut col_del_count = 0u32; + for i in 1..y { + let seed = USER_SEED + i; + let _ = create_funded_delegator::( + "delegator", + seed, + 0u32.into(), + collator.clone(), + true, + col_del_count, + )?; + col_del_count += 1u32; + } + }: _(RawOrigin::Signed(caller.clone()), collator, bond, col_del_count, del_del_count) + verify { + assert!(Pallet::::is_delegator(&caller)); + } + + schedule_leave_delegators { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, _) = create_funded_user::("caller", USER_SEED, 0u32.into()); + let bond = <::MinDelegatorStk as Get>>::get(); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + bond, + 0u32, + 0u32 + )?; + }: _(RawOrigin::Signed(caller.clone())) + verify { + assert!( + Pallet::::delegation_scheduled_requests(&collator) + .iter() + .any(|r| r.delegator == caller && matches!(r.action, DelegationAction::Revoke(_))) + ); + } + + execute_leave_delegators { + let x in 2..<::MaxDelegationsPerDelegator as Get>::get(); + // Worst Case is full of delegations before execute exit + let mut collators: Vec = Vec::new(); + // Initialize MaxDelegationsPerDelegator collator candidates + for i in 1..x { + let seed = USER_SEED - i; + let collator = create_funded_collator::( + "collator", + seed, + 0u32.into(), + true, + collators.len() as u32 + 1u32 + )?; + collators.push(collator.clone()); + } + let bond = <::MinDelegatorStk as Get>>::get(); + let need = bond * (collators.len() as u32).into(); + let default_minted = min_candidate_stk::(); + let need: BalanceOf = if need > default_minted { + need - default_minted + } else { + 0u32.into() + }; + // Fund the delegator + let (caller, _) = create_funded_user::("caller", USER_SEED, need); + // Delegation count + let mut delegation_count = 0u32; + let author = collators[0].clone(); + // Nominate MaxDelegationsPerDelegators collator candidates + for col in collators { + Pallet::::delegate( + RawOrigin::Signed(caller.clone()).into(), + col, + bond, + 0u32, + delegation_count + )?; + delegation_count += 1u32; + } + Pallet::::schedule_leave_delegators(RawOrigin::Signed(caller.clone()).into())?; + roll_to_and_author::(T::DelegationBondLessDelay::get(), author); + }: _(RawOrigin::Signed(caller.clone()), caller.clone(), delegation_count) + verify { + assert!(Pallet::::delegator_state(&caller).is_none()); + } + + cancel_leave_delegators { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, _) = create_funded_user::("caller", USER_SEED, 0u32.into()); + let bond = <::MinDelegatorStk as Get>>::get(); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + bond, + 0u32, + 0u32 + )?; + Pallet::::schedule_leave_delegators(RawOrigin::Signed(caller.clone()).into())?; + }: _(RawOrigin::Signed(caller.clone())) + verify { + assert!(Pallet::::delegator_state(&caller).expect("must exist").is_active()); + } + + schedule_revoke_delegation { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, _) = create_funded_user::("caller", USER_SEED, 0u32.into()); + let bond = <::MinDelegatorStk as Get>>::get(); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + bond, + 0u32, + 0u32 + )?; + }: _(RawOrigin::Signed(caller.clone()), collator.clone()) + verify { + assert_eq!( + Pallet::::delegation_scheduled_requests(&collator), + vec![ScheduledRequest { + delegator: caller, + when_executable: T::RevokeDelegationDelay::get() + 1, + action: DelegationAction::Revoke(bond), + }], + ); + } + + delegator_bond_more { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, _) = create_funded_user::("caller", USER_SEED, 0u32.into()); + let bond = <::MinDelegatorStk as Get>>::get(); + Pallet::::delegate( + RawOrigin::Signed(caller.clone()).into(), + collator.clone(), + bond, + 0u32, + 0u32 + )?; + }: _(RawOrigin::Signed(caller.clone()), collator.clone(), bond) + verify { + let expected_bond = bond * 2u32.into(); + assert_eq!( + Pallet::::delegator_state(&caller).expect("candidate was created, qed").total, + expected_bond, + ); + } + + schedule_delegator_bond_less { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, total) = create_funded_user::("caller", USER_SEED, 0u32.into()); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + total, + 0u32, + 0u32 + )?; + let bond_less = <::MinDelegatorStk as Get>>::get(); + }: _(RawOrigin::Signed(caller.clone()), collator.clone(), bond_less) + verify { + let state = Pallet::::delegator_state(&caller) + .expect("just request bonded less so exists"); + assert_eq!( + Pallet::::delegation_scheduled_requests(&collator), + vec![ScheduledRequest { + delegator: caller, + when_executable: T::DelegationBondLessDelay::get() + 1, + action: DelegationAction::Decrease(bond_less), + }], + ); + } + + execute_revoke_delegation { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, _) = create_funded_user::("caller", USER_SEED, 0u32.into()); + let bond = <::MinDelegatorStk as Get>>::get(); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + bond, + 0u32, + 0u32 + )?; + Pallet::::schedule_revoke_delegation(RawOrigin::Signed( + caller.clone()).into(), + collator.clone() + )?; + roll_to_and_author::(T::RevokeDelegationDelay::get(), collator.clone()); + }: { + Pallet::::execute_delegation_request( + RawOrigin::Signed(caller.clone()).into(), + caller.clone(), + collator.clone() + )?; + } verify { + assert!( + !Pallet::::is_delegator(&caller) + ); + } + + execute_delegator_bond_less { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, total) = create_funded_user::("caller", USER_SEED, 0u32.into()); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + total, + 0u32, + 0u32 + )?; + let bond_less = <::MinDelegatorStk as Get>>::get(); + Pallet::::schedule_delegator_bond_less( + RawOrigin::Signed(caller.clone()).into(), + collator.clone(), + bond_less + )?; + roll_to_and_author::(T::DelegationBondLessDelay::get(), collator.clone()); + }: { + Pallet::::execute_delegation_request( + RawOrigin::Signed(caller.clone()).into(), + caller.clone(), + collator.clone() + )?; + } verify { + let expected = total - bond_less; + assert_eq!( + Pallet::::delegator_state(&caller).expect("candidate was created, qed").total, + expected, + ); + } + + cancel_revoke_delegation { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, _) = create_funded_user::("caller", USER_SEED, 0u32.into()); + let bond = <::MinDelegatorStk as Get>>::get(); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + bond, + 0u32, + 0u32 + )?; + Pallet::::schedule_revoke_delegation( + RawOrigin::Signed(caller.clone()).into(), + collator.clone() + )?; + }: { + Pallet::::cancel_delegation_request( + RawOrigin::Signed(caller.clone()).into(), + collator.clone() + )?; + } verify { + assert!( + !Pallet::::delegation_scheduled_requests(&collator) + .iter() + .any(|x| &x.delegator == &caller) + ); + } + + cancel_delegator_bond_less { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let (caller, total) = create_funded_user::("caller", USER_SEED, 0u32.into()); + Pallet::::delegate(RawOrigin::Signed( + caller.clone()).into(), + collator.clone(), + total, + 0u32, + 0u32 + )?; + let bond_less = <::MinDelegatorStk as Get>>::get(); + Pallet::::schedule_delegator_bond_less( + RawOrigin::Signed(caller.clone()).into(), + collator.clone(), + bond_less + )?; + roll_to_and_author::(2, collator.clone()); + }: { + Pallet::::cancel_delegation_request( + RawOrigin::Signed(caller.clone()).into(), + collator.clone() + )?; + } verify { + assert!( + !Pallet::::delegation_scheduled_requests(&collator) + .iter() + .any(|x| &x.delegator == &caller) + ); + } + + // ON_INITIALIZE + + prepare_staking_payouts { + let reward_delay = <::RewardPaymentDelay as Get>::get(); + let round = reward_delay + 2u32; + let payout_round = round - reward_delay; + // may need: + // > + // > + // > + // ensure parachain bond account exists so that deposit_into_existing succeeds + >::insert(payout_round, 100); + >::insert(payout_round, min_candidate_stk::()); + + // set an account in the bond config so that we will measure the payout to it + let account = create_funded_user::( + "parachain_bond", + 0, + min_candidate_stk::(), + ).0; + >::put(ParachainBondConfig { + account, + percent: Percent::from_percent(50), + }); + + }: { Pallet::::prepare_staking_payouts(round); } + verify { + } + + get_rewardable_delegators { + let y in 0..<::MaxDelegationsPerDelegator as Get>::get(); // num delegators + + let high_inflation: Range = Range { + min: Perbill::one(), + ideal: Perbill::one(), + max: Perbill::one(), + }; + Pallet::::set_inflation(RawOrigin::Root.into(), high_inflation.clone())?; + Pallet::::set_blocks_per_round(RawOrigin::Root.into(), 101u32)?; + Pallet::::set_total_selected(RawOrigin::Root.into(), 100u32)?; + + let collator = create_funded_collator::( + "collator", + 0, + min_candidate_stk::() * 1_000_000u32.into(), + true, + 1, + )?; + + // create delegators + for i in 0..y { + let seed = USER_SEED + i + 1; + let delegator = create_funded_delegator::( + "delegator", + seed, + min_candidate_stk::() * 1_000_000u32.into(), + collator.clone(), + true, + i, + )?; + } + + let mut _results = None; + + }: { _results = Some(Pallet::::get_rewardable_delegators(&collator)); } + verify { + let counted_delegations = _results.expect("get_rewardable_delegators returned some results"); + assert!(counted_delegations.uncounted_stake == 0u32.into()); + assert!(counted_delegations.rewardable_delegations.len() as u32 == y); + let top_delegations = >::get(collator.clone()) + .expect("delegations were set for collator through delegate() calls"); + assert!(top_delegations.delegations.len() as u32 == y); + } + + select_top_candidates { + let x in 0..50; // num collators + let y in 0..<::MaxDelegationsPerDelegator as Get>::get(); // num delegators + + let high_inflation: Range = Range { + min: Perbill::one(), + ideal: Perbill::one(), + max: Perbill::one(), + }; + Pallet::::set_inflation(RawOrigin::Root.into(), high_inflation.clone())?; + Pallet::::set_blocks_per_round(RawOrigin::Root.into(), 101u32)?; + Pallet::::set_total_selected(RawOrigin::Root.into(), 100u32)?; + + let mut seed = USER_SEED + 1; + + for _ in 0..x { + let collator = create_funded_collator::( + "collator", + seed, + min_candidate_stk::() * 1_000_000u32.into(), + true, + 999999, + )?; + seed += 1; + + // create delegators + for _ in 0..y { + let delegator = create_funded_delegator::( + "delegator", + seed, + min_candidate_stk::() * 1_000_000u32.into(), + collator.clone(), + true, + 9999999, + )?; + seed += 1; + } + } + + }: { Pallet::::select_top_candidates(1); } + verify { + } + + pay_one_collator_reward { + // y controls number of delegations, its maximum per collator is the max top delegations + let y in 0..<::MaxTopDelegationsPerCandidate as Get>::get(); + + // must come after 'let foo in 0..` statements for macro + use crate::{ + DelayedPayout, DelayedPayouts, AtStake, CollatorSnapshot, BondWithAutoCompound, Points, + AwardedPts, + }; + + let before_running_round_index = Pallet::::round().current; + let initial_stake_amount = min_candidate_stk::() * 1_000_000u32.into(); + + let mut total_staked = 0u32.into(); + + // initialize our single collator + let sole_collator = create_funded_collator::( + "collator", + 0, + initial_stake_amount, + true, + 1u32, + )?; + total_staked += initial_stake_amount; + + // generate funded collator accounts + let mut delegators: Vec = Vec::new(); + for i in 0..y { + let seed = USER_SEED + i; + let delegator = create_funded_delegator::( + "delegator", + seed, + initial_stake_amount, + sole_collator.clone(), + true, + delegators.len() as u32, + )?; + delegators.push(delegator); + total_staked += initial_stake_amount; + } + + // rather than roll through rounds in order to initialize the storage we want, we set it + // directly and then call pay_one_collator_reward directly. + + let round_for_payout = 5; + >::insert(&round_for_payout, DelayedPayout { + // NOTE: round_issuance is not correct here, but it doesn't seem to cause problems + round_issuance: 1000u32.into(), + total_staking_reward: total_staked, + collator_commission: Perbill::from_rational(1u32, 100u32), + }); + + let mut delegations: Vec>> = Vec::new(); + for delegator in &delegators { + delegations.push(BondWithAutoCompound { + owner: delegator.clone(), + amount: 100u32.into(), + auto_compound: Percent::zero(), + }); + } + + >::insert(round_for_payout, &sole_collator, CollatorSnapshot { + bond: 1_000u32.into(), + delegations, + total: 1_000_000u32.into(), + }); + + >::insert(round_for_payout, 100); + >::insert(round_for_payout, &sole_collator, 20); + + }: { + let round_for_payout = 5; + // TODO: this is an extra read right here (we should whitelist it?) + let payout_info = Pallet::::delayed_payouts(round_for_payout).expect("payout expected"); + let result = Pallet::::pay_one_collator_reward(round_for_payout, payout_info); + // TODO: how to keep this in scope so it can be done in verify block? + assert!(matches!(result.0, RewardPayment::Paid)); + } + verify { + // collator should have been paid + assert!( + T::Currency::balance(&sole_collator) > initial_stake_amount, + "collator should have been paid in pay_one_collator_reward" + ); + // nominators should have been paid + for delegator in &delegators { + assert!( + T::Currency::balance(&delegator) > initial_stake_amount, + "delegator should have been paid in pay_one_collator_reward" + ); + } + } + + base_on_initialize { + let collator: T::AccountId = create_funded_collator::( + "collator", + USER_SEED, + 0u32.into(), + true, + 1u32 + )?; + let start = >::block_number(); + parachain_staking_on_finalize::(collator.clone()); + >::on_finalize(start); + >::set_block_number( + start + 1u32.into() + ); + let end = >::block_number(); + >::on_initialize(end); + }: { Pallet::::on_initialize(end); } + verify { + // Round transitions + assert_eq!(start + 1u32.into(), end); + } + + set_auto_compound { + // x controls number of distinct auto-compounding delegations the prime collator will have + // y controls number of distinct delegations the prime delegator will have + let x in 0..<::MaxTopDelegationsPerCandidate as Get>::get(); + let y in 0..<::MaxDelegationsPerDelegator as Get>::get(); + + use crate::auto_compound::AutoCompoundDelegations; + + let min_candidate_stake = min_candidate_stk::(); + let min_delegator_stake = min_delegator_stk::(); + let mut seed = Seed::new(); + + // initialize the prime collator + let prime_candidate = create_funded_collator::( + "collator", + seed.take(), + min_candidate_stake, + true, + 1, + )?; + + // initialize the prime delegator + let prime_delegator = create_funded_delegator::( + "delegator", + seed.take(), + min_delegator_stake * (y+1).into(), + prime_candidate.clone(), + true, + 0, + )?; + + // have x-1 distinct auto-compounding delegators delegate to prime collator + // we directly set the storage, since benchmarks don't work when the same extrinsic is + // called from within the benchmark. + let mut auto_compounding_state = >::get_storage(&prime_candidate); + for i in 1..x { + let delegator = create_funded_delegator::( + "delegator", + seed.take(), + min_delegator_stake, + prime_candidate.clone(), + true, + i, + )?; + auto_compounding_state.set_for_delegator( + delegator, + Percent::from_percent(100), + ).expect("must succeed"); + } + auto_compounding_state.set_storage(&prime_candidate); + + // delegate to y-1 distinct collators from the prime delegator + for i in 1..y { + let collator = create_funded_collator::( + "collator", + seed.take(), + min_candidate_stake, + true, + i+1, + )?; + Pallet::::delegate( + RawOrigin::Signed(prime_delegator.clone()).into(), + collator, + min_delegator_stake, + 0, + i, + )?; + } + }: { + Pallet::::set_auto_compound( + RawOrigin::Signed(prime_delegator.clone()).into(), + prime_candidate.clone(), + Percent::from_percent(50), + x, + y+1, + )?; + } + verify { + let actual_auto_compound = >::get_storage(&prime_candidate) + .get_for_delegator(&prime_delegator); + let expected_auto_compound = Some(Percent::from_percent(50)); + assert_eq!( + expected_auto_compound, + actual_auto_compound, + "delegation must have an auto-compound entry", + ); + } + + delegate_with_auto_compound { + // x controls number of distinct delegations the prime collator will have + // y controls number of distinct auto-compounding delegations the prime collator will have + // z controls number of distinct delegations the prime delegator will have + let x in 0..(<::MaxTopDelegationsPerCandidate as Get>::get() + + <::MaxBottomDelegationsPerCandidate as Get>::get()); + let y in 0..<::MaxTopDelegationsPerCandidate as Get>::get() + + <::MaxBottomDelegationsPerCandidate as Get>::get(); + let z in 0..<::MaxDelegationsPerDelegator as Get>::get(); + + use crate::auto_compound::AutoCompoundDelegations; + + let min_candidate_stake = min_candidate_stk::(); + let min_delegator_stake = min_delegator_stk::(); + let mut seed = Seed::new(); + + // initialize the prime collator + let prime_candidate = create_funded_collator::( + "collator", + seed.take(), + min_candidate_stake, + true, + 1, + )?; + + // initialize the future delegator + let (prime_delegator, _) = create_funded_user::( + "delegator", + seed.take(), + min_delegator_stake * (z+1).into(), + ); + + // have x-1 distinct delegators delegate to prime collator, of which y are auto-compounding. + // we can directly set the storage here. + let auto_compound_z = x * y / 100; + for i in 1..x { + let delegator = create_funded_delegator::( + "delegator", + seed.take(), + min_delegator_stake, + prime_candidate.clone(), + true, + i, + )?; + if i <= y { + Pallet::::set_auto_compound( + RawOrigin::Signed(delegator.clone()).into(), + prime_candidate.clone(), + Percent::from_percent(100), + i+1, + i, + )?; + } + } + + // delegate to z-1 distinct collators from the prime delegator + for i in 1..z { + let collator = create_funded_collator::( + "collator", + seed.take(), + min_candidate_stake, + true, + i+1, + )?; + Pallet::::delegate( + RawOrigin::Signed(prime_delegator.clone()).into(), + collator, + min_delegator_stake, + 0, + i, + )?; + } + }: { + Pallet::::delegate_with_auto_compound( + RawOrigin::Signed(prime_delegator.clone()).into(), + prime_candidate.clone(), + min_delegator_stake, + Percent::from_percent(50), + x, + y, + z, + )?; + } + verify { + assert!(Pallet::::is_delegator(&prime_delegator)); + let actual_auto_compound = >::get_storage(&prime_candidate) + .get_for_delegator(&prime_delegator); + let expected_auto_compound = Some(Percent::from_percent(50)); + assert_eq!( + expected_auto_compound, + actual_auto_compound, + "delegation must have an auto-compound entry", + ); + } + + mint_collator_reward { + let mut seed = Seed::new(); + let collator = create_funded_collator::( + "collator", + seed.take(), + 1u32.into(), + true, + 1, + )?; + let original_free_balance = T::Currency::balance(&collator); + let pay_master = T::PayMaster::get(); + T::Currency::set_balance(&pay_master, T::Currency::minimum_balance() + 50u32.into()); + + }: { + Pallet::::mint_collator_reward(1u32.into(), collator.clone(), 50u32.into()) + } + verify { + assert_eq!(T::Currency::balance(&collator), original_free_balance + 50u32.into()); + } +} + +#[cfg(test)] +mod tests { + use crate::{benchmarks::*, mock::Test}; + use frame_support::assert_ok; + use sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + TestExternalities::new(t) + } + + #[test] + fn bench_set_staking_expectations() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_staking_expectations()); + }); + } + + #[test] + fn bench_set_inflation() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_inflation()); + }); + } + + #[test] + fn bench_set_parachain_bond_account() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_parachain_bond_account()); + }); + } + + #[test] + fn bench_set_parachain_bond_reserve_percent() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_parachain_bond_reserve_percent()); + }); + } + + #[test] + fn bench_set_total_selected() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_total_selected()); + }); + } + + #[test] + fn bench_set_collator_commission() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_collator_commission()); + }); + } + + #[test] + fn bench_set_blocks_per_round() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_set_blocks_per_round()); + }); + } + + #[test] + fn bench_join_candidates() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_join_candidates()); + }); + } + + #[test] + fn bench_schedule_leave_candidates() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_schedule_leave_candidates()); + }); + } + + #[test] + fn bench_execute_leave_candidates() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_execute_leave_candidates()); + }); + } + + #[test] + fn bench_cancel_leave_candidates() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_cancel_leave_candidates()); + }); + } + + #[test] + fn bench_go_offline() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_go_offline()); + }); + } + + #[test] + fn bench_go_online() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_go_online()); + }); + } + + #[test] + fn bench_candidate_bond_more() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_candidate_bond_more()); + }); + } + + #[test] + fn bench_schedule_candidate_bond_less() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_schedule_candidate_bond_less()); + }); + } + + #[test] + fn bench_execute_candidate_bond_less() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_execute_candidate_bond_less()); + }); + } + + #[test] + fn bench_cancel_candidate_bond_less() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_cancel_candidate_bond_less()); + }); + } + + #[test] + fn bench_delegate() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_delegate()); + }); + } + + #[test] + fn bench_schedule_leave_delegators() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_schedule_leave_delegators()); + }); + } + + #[test] + fn bench_execute_leave_delegators() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_execute_leave_delegators()); + }); + } + + #[test] + fn bench_cancel_leave_delegators() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_cancel_leave_delegators()); + }); + } + + #[test] + fn bench_schedule_revoke_delegation() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_schedule_revoke_delegation()); + }); + } + + #[test] + fn bench_delegator_bond_more() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_delegator_bond_more()); + }); + } + + #[test] + fn bench_schedule_delegator_bond_less() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_schedule_delegator_bond_less()); + }); + } + + #[test] + fn bench_execute_revoke_delegation() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_execute_revoke_delegation()); + }); + } + + #[test] + fn bench_execute_delegator_bond_less() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_execute_delegator_bond_less()); + }); + } + + #[test] + fn bench_cancel_revoke_delegation() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_cancel_revoke_delegation()); + }); + } + + #[test] + fn bench_cancel_delegator_bond_less() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_cancel_delegator_bond_less()); + }); + } + + #[test] + fn bench_base_on_initialize() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::::test_benchmark_base_on_initialize()); + }); + } +} + +impl_benchmark_test_suite!(Pallet, crate::benchmarks::tests::new_test_ext(), crate::mock::Test); diff --git a/pallets/parachain-staking/src/delegation_requests.rs b/pallets/parachain-staking/src/delegation_requests.rs new file mode 100644 index 0000000..253a6d3 --- /dev/null +++ b/pallets/parachain-staking/src/delegation_requests.rs @@ -0,0 +1,576 @@ +// 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 . + +//! Scheduled requests functionality for delegators + +use crate::{ + auto_compound::AutoCompoundDelegations, + pallet::{ + BalanceOf, CandidateInfo, Config, DelegationScheduledRequests, DelegatorState, Error, Event, Pallet, Round, + RoundIndex, Total, + }, + Delegator, DelegatorStatus, +}; +use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, traits::Get}; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::{traits::Saturating, RuntimeDebug}; +use sp_std::{vec, vec::Vec}; + +/// An action that can be performed upon a delegation +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)] +pub enum DelegationAction { + Revoke(Balance), + Decrease(Balance), +} + +impl DelegationAction { + /// Returns the wrapped amount value. + pub fn amount(&self) -> Balance { + match self { + DelegationAction::Revoke(amount) => *amount, + DelegationAction::Decrease(amount) => *amount, + } + } +} + +/// Represents a scheduled request that define a [DelegationAction]. The request is executable +/// iff the provided [RoundIndex] is achieved. +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)] +pub struct ScheduledRequest { + pub delegator: AccountId, + pub when_executable: RoundIndex, + pub action: DelegationAction, +} + +/// Represents a cancelled scheduled request for emitting an event. +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct CancelledScheduledRequest { + pub when_executable: RoundIndex, + pub action: DelegationAction, +} + +impl From> for CancelledScheduledRequest { + fn from(request: ScheduledRequest) -> Self { + CancelledScheduledRequest { when_executable: request.when_executable, action: request.action } + } +} + +impl Pallet { + /// Schedules a [DelegationAction::Revoke] for the delegator, towards a given collator. + pub(crate) fn delegation_schedule_revoke( + collator: T::AccountId, + delegator: T::AccountId, + ) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + let mut scheduled_requests = >::get(&collator); + + ensure!( + !scheduled_requests.iter().any(|req| req.delegator == delegator), + >::PendingDelegationRequestAlreadyExists, + ); + + let bonded_amount = state.get_bond_amount(&collator).ok_or(>::DelegationDNE)?; + let now = >::get().current; + let when = now.saturating_add(T::RevokeDelegationDelay::get()); + scheduled_requests.push(ScheduledRequest { + delegator: delegator.clone(), + action: DelegationAction::Revoke(bonded_amount), + when_executable: when, + }); + state.less_total = state.less_total.saturating_add(bonded_amount); + >::insert(collator.clone(), scheduled_requests); + >::insert(delegator.clone(), state); + + Self::deposit_event(Event::DelegationRevocationScheduled { + round: now, + delegator, + candidate: collator, + scheduled_exit: when, + }); + Ok(().into()) + } + + /// Schedules a [DelegationAction::Decrease] for the delegator, towards a given collator. + pub(crate) fn delegation_schedule_bond_decrease( + collator: T::AccountId, + delegator: T::AccountId, + decrease_amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + let mut scheduled_requests = >::get(&collator); + + ensure!( + !scheduled_requests.iter().any(|req| req.delegator == delegator), + >::PendingDelegationRequestAlreadyExists, + ); + + let bonded_amount = state.get_bond_amount(&collator).ok_or(>::DelegationDNE)?; + ensure!(bonded_amount > decrease_amount, >::DelegatorBondBelowMin); + let new_amount: BalanceOf = bonded_amount - decrease_amount; + ensure!(new_amount >= T::MinDelegation::get(), >::DelegationBelowMin); + + // Net Total is total after pending orders are executed + let net_total = state.total().saturating_sub(state.less_total); + // Net Total is always >= MinDelegatorStk + let max_subtracted_amount = net_total.saturating_sub(T::MinDelegatorStk::get()); + ensure!(decrease_amount <= max_subtracted_amount, >::DelegatorBondBelowMin); + + let now = >::get().current; + let when = now.saturating_add(T::RevokeDelegationDelay::get()); + scheduled_requests.push(ScheduledRequest { + delegator: delegator.clone(), + action: DelegationAction::Decrease(decrease_amount), + when_executable: when, + }); + state.less_total = state.less_total.saturating_add(decrease_amount); + >::insert(collator.clone(), scheduled_requests); + >::insert(delegator.clone(), state); + + Self::deposit_event(Event::DelegationDecreaseScheduled { + delegator, + candidate: collator, + amount_to_decrease: decrease_amount, + execute_round: when, + }); + Ok(().into()) + } + + /// Cancels the delegator's existing [ScheduledRequest] towards a given collator. + pub(crate) fn delegation_cancel_request( + collator: T::AccountId, + delegator: T::AccountId, + ) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + let mut scheduled_requests = >::get(&collator); + + let request = Self::cancel_request_with_state(&delegator, &mut state, &mut scheduled_requests) + .ok_or(>::PendingDelegationRequestDNE)?; + + >::insert(collator.clone(), scheduled_requests); + >::insert(delegator.clone(), state); + + Self::deposit_event(Event::CancelledDelegationRequest { + delegator, + collator, + cancelled_request: request.into(), + }); + Ok(().into()) + } + + fn cancel_request_with_state( + delegator: &T::AccountId, + state: &mut Delegator>, + scheduled_requests: &mut Vec>>, + ) -> Option>> { + let request_idx = scheduled_requests.iter().position(|req| &req.delegator == delegator)?; + + let request = scheduled_requests.remove(request_idx); + let amount = request.action.amount(); + state.less_total = state.less_total.saturating_sub(amount); + Some(request) + } + + /// Executes the delegator's existing [ScheduledRequest] towards a given collator. + pub(crate) fn delegation_execute_scheduled_request( + collator: T::AccountId, + delegator: T::AccountId, + ) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + let mut scheduled_requests = >::get(&collator); + let request_idx = scheduled_requests + .iter() + .position(|req| req.delegator == delegator) + .ok_or(>::PendingDelegationRequestDNE)?; + let request = &scheduled_requests[request_idx]; + + let now = >::get().current; + ensure!(request.when_executable <= now, >::PendingDelegationRequestNotDueYet); + + match request.action { + DelegationAction::Revoke(amount) => { + // revoking last delegation => leaving set of delegators + let leaving = if state.delegations.0.len() == 1usize { + true + } else { + ensure!( + state.total().saturating_sub(T::MinDelegatorStk::get()) >= amount, + >::DelegatorBondBelowMin + ); + false + }; + + // remove from pending requests + let amount = scheduled_requests.remove(request_idx).action.amount(); + state.less_total = state.less_total.saturating_sub(amount); + + // remove delegation from delegator state + state.rm_delegation::(&collator); + + // remove delegation from auto-compounding info + >::remove_auto_compound(&collator, &delegator); + + // remove delegation from collator state delegations + Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)?; + Self::deposit_event(Event::DelegationRevoked { + delegator: delegator.clone(), + candidate: collator.clone(), + unstaked_amount: amount, + }); + + >::insert(collator, scheduled_requests); + if leaving { + >::remove(&delegator); + Self::deposit_event(Event::DelegatorLeft { delegator, unstaked_amount: amount }); + } else { + >::insert(&delegator, state); + } + Ok(().into()) + }, + DelegationAction::Decrease(_) => { + // remove from pending requests + let amount = scheduled_requests.remove(request_idx).action.amount(); + state.less_total = state.less_total.saturating_sub(amount); + + // decrease delegation + for bond in &mut state.delegations.0 { + if bond.owner == collator { + return if bond.amount > amount { + let amount_before: BalanceOf = bond.amount; + bond.amount = bond.amount.saturating_sub(amount); + let mut collator_info = + >::get(&collator).ok_or(>::CandidateDNE)?; + + state.total_sub_if::(amount, |total| { + let new_total: BalanceOf = total; + ensure!(new_total >= T::MinDelegation::get(), >::DelegationBelowMin); + ensure!(new_total >= T::MinDelegatorStk::get(), >::DelegatorBondBelowMin); + + Ok(()) + })?; + + // need to go into decrease_delegation + let in_top = collator_info.decrease_delegation::( + &collator, + delegator.clone(), + amount_before, + amount, + )?; + >::insert(&collator, collator_info); + let new_total_staked = >::get().saturating_sub(amount); + >::put(new_total_staked); + + >::insert(collator.clone(), scheduled_requests); + >::insert(delegator.clone(), state); + Self::deposit_event(Event::DelegationDecreased { + delegator, + candidate: collator.clone(), + amount, + in_top, + }); + Ok(().into()) + } else { + // must rm entire delegation if bond.amount <= less or cancel request + Err(>::DelegationBelowMin.into()) + }; + } + } + Err(>::DelegationDNE.into()) + }, + } + } + + /// Schedules [DelegationAction::Revoke] for the delegator, towards all delegated collator. + /// The last fulfilled request causes the delegator to leave the set of delegators. + pub(crate) fn delegator_schedule_revoke_all(delegator: T::AccountId) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + let mut updated_scheduled_requests = vec![]; + let now = >::get().current; + let when = now.saturating_add(T::LeaveDelegatorsDelay::get()); + + // lazy migration for DelegatorStatus::Leaving + #[allow(deprecated)] + if matches!(state.status, DelegatorStatus::Leaving(_)) { + state.status = DelegatorStatus::Active; + >::insert(delegator.clone(), state.clone()); + } + + // it is assumed that a multiple delegations to the same collator does not exist, else this + // will cause a bug - the last duplicate delegation update will be the only one applied. + let mut existing_revoke_count = 0; + for bond in state.delegations.0.clone() { + let collator = bond.owner; + let bonded_amount = bond.amount; + let mut scheduled_requests = >::get(&collator); + + // cancel any existing requests + let request = Self::cancel_request_with_state(&delegator, &mut state, &mut scheduled_requests); + let request = match request { + Some(revoke_req) if matches!(revoke_req.action, DelegationAction::Revoke(_)) => { + existing_revoke_count += 1; + revoke_req // re-insert the same Revoke request + }, + _ => ScheduledRequest { + delegator: delegator.clone(), + action: DelegationAction::Revoke(bonded_amount), + when_executable: when, + }, + }; + + scheduled_requests.push(request); + state.less_total = state.less_total.saturating_add(bonded_amount); + updated_scheduled_requests.push((collator, scheduled_requests)); + } + + if existing_revoke_count == state.delegations.0.len() { + return Err(>::DelegatorAlreadyLeaving.into()); + } + + updated_scheduled_requests.into_iter().for_each(|(collator, scheduled_requests)| { + >::insert(collator, scheduled_requests); + }); + + >::insert(delegator.clone(), state); + Self::deposit_event(Event::DelegatorExitScheduled { round: now, delegator, scheduled_exit: when }); + Ok(().into()) + } + + /// Cancels every [DelegationAction::Revoke] request for a delegator towards a collator. + /// Each delegation must have a [DelegationAction::Revoke] scheduled that must be allowed to be + /// executed in the current round, for this function to succeed. + pub(crate) fn delegator_cancel_scheduled_revoke_all(delegator: T::AccountId) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + let mut updated_scheduled_requests = vec![]; + + // backwards compatible handling for DelegatorStatus::Leaving + #[allow(deprecated)] + if matches!(state.status, DelegatorStatus::Leaving(_)) { + state.status = DelegatorStatus::Active; + >::insert(delegator.clone(), state.clone()); + Self::deposit_event(Event::DelegatorExitCancelled { delegator }); + return Ok(().into()); + } + + // pre-validate that all delegations have a Revoke request. + for bond in &state.delegations.0 { + let collator = bond.owner.clone(); + let scheduled_requests = >::get(&collator); + scheduled_requests + .iter() + .find(|req| req.delegator == delegator && matches!(req.action, DelegationAction::Revoke(_))) + .ok_or(>::DelegatorNotLeaving)?; + } + + // cancel all requests + for bond in state.delegations.0.clone() { + let collator = bond.owner.clone(); + let mut scheduled_requests = >::get(&collator); + Self::cancel_request_with_state(&delegator, &mut state, &mut scheduled_requests); + updated_scheduled_requests.push((collator, scheduled_requests)); + } + + updated_scheduled_requests.into_iter().for_each(|(collator, scheduled_requests)| { + >::insert(collator, scheduled_requests); + }); + + >::insert(delegator.clone(), state); + Self::deposit_event(Event::DelegatorExitCancelled { delegator }); + + Ok(().into()) + } + + /// Executes every [DelegationAction::Revoke] request for a delegator towards a collator. + /// Each delegation must have a [DelegationAction::Revoke] scheduled that must be allowed to be + /// executed in the current round, for this function to succeed. + pub(crate) fn delegator_execute_scheduled_revoke_all( + delegator: T::AccountId, + delegation_count: u32, + ) -> DispatchResultWithPostInfo { + let mut state = >::get(&delegator).ok_or(>::DelegatorDNE)?; + ensure!( + delegation_count >= (state.delegations.0.len() as u32), + Error::::TooLowDelegationCountToLeaveDelegators + ); + let now = >::get().current; + + // backwards compatible handling for DelegatorStatus::Leaving + #[allow(deprecated)] + if let DelegatorStatus::Leaving(when) = state.status { + ensure!(>::get().current >= when, Error::::DelegatorCannotLeaveYet); + + for bond in state.delegations.0.clone() { + if let Err(error) = Self::delegator_leaves_candidate(bond.owner.clone(), delegator.clone(), bond.amount) + { + log::warn!("STORAGE CORRUPTED \nDelegator leaving collator failed with error: {:?}", error); + } + + Self::delegation_remove_request_with_state(&bond.owner, &delegator, &mut state); + >::remove_auto_compound(&bond.owner, &delegator); + } + >::remove(&delegator); + Self::deposit_event(Event::DelegatorLeft { delegator, unstaked_amount: state.total }); + return Ok(().into()); + } + + let mut validated_scheduled_requests = vec![]; + // pre-validate that all delegations have a Revoke request that can be executed now. + for bond in &state.delegations.0 { + let scheduled_requests = >::get(&bond.owner); + let request_idx = scheduled_requests + .iter() + .position(|req| req.delegator == delegator && matches!(req.action, DelegationAction::Revoke(_))) + .ok_or(>::DelegatorNotLeaving)?; + let request = &scheduled_requests[request_idx]; + + ensure!(request.when_executable <= now, >::DelegatorCannotLeaveYet); + + validated_scheduled_requests.push((bond.clone(), scheduled_requests, request_idx)) + } + + let mut updated_scheduled_requests = vec![]; + // we do not update the delegator state, since the it will be completely removed + for (bond, mut scheduled_requests, request_idx) in validated_scheduled_requests { + let collator = bond.owner; + + if let Err(error) = Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), bond.amount) { + log::warn!( + "STORAGE CORRUPTED \nDelegator {:?} leaving collator failed with error: {:?}", + delegator, + error + ); + } + + // remove the scheduled request, since it is fulfilled + scheduled_requests.remove(request_idx).action.amount(); + updated_scheduled_requests.push((collator.clone(), scheduled_requests)); + + // remove the auto-compounding entry for the delegation + >::remove_auto_compound(&collator, &delegator); + } + + // set state.total so that state.adjust_bond_lock will remove lock + let unstaked_amount = state.total(); + state.total_sub::(unstaked_amount)?; + + updated_scheduled_requests.into_iter().for_each(|(collator, scheduled_requests)| { + >::insert(collator, scheduled_requests); + }); + + Self::deposit_event(Event::DelegatorLeft { delegator: delegator.clone(), unstaked_amount }); + >::remove(&delegator); + + Ok(().into()) + } + + /// Removes the delegator's existing [ScheduledRequest] towards a given collator, if exists. + /// The state needs to be persisted by the caller of this function. + pub(crate) fn delegation_remove_request_with_state( + collator: &T::AccountId, + delegator: &T::AccountId, + state: &mut Delegator>, + ) { + let mut scheduled_requests = >::get(collator); + + let maybe_request_idx = scheduled_requests.iter().position(|req| &req.delegator == delegator); + + if let Some(request_idx) = maybe_request_idx { + let request = scheduled_requests.remove(request_idx); + let amount = request.action.amount(); + state.less_total = state.less_total.saturating_sub(amount); + >::insert(collator, scheduled_requests); + } + } + + /// Returns true if a [ScheduledRequest] exists for a given delegation + pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool { + >::get(collator).iter().any(|req| &req.delegator == delegator) + } + + /// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation + pub fn delegation_request_revoke_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool { + >::get(collator) + .iter() + .any(|req| &req.delegator == delegator && matches!(req.action, DelegationAction::Revoke(_))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{mock::Test, set::OrderedSet, Bond}; + + #[test] + fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() { + let mut state = Delegator { + id: 1, + delegations: OrderedSet::from(vec![Bond { amount: 100, owner: 2 }]), + total: 100, + less_total: 100, + status: crate::DelegatorStatus::Active, + }; + let mut scheduled_requests = vec![ + ScheduledRequest { delegator: 1, when_executable: 1, action: DelegationAction::Revoke(100) }, + ScheduledRequest { delegator: 2, when_executable: 1, action: DelegationAction::Decrease(50) }, + ]; + let removed_request = >::cancel_request_with_state(&1, &mut state, &mut scheduled_requests); + + assert_eq!( + removed_request, + Some(ScheduledRequest { delegator: 1, when_executable: 1, action: DelegationAction::Revoke(100) }) + ); + assert_eq!( + scheduled_requests, + vec![ScheduledRequest { delegator: 2, when_executable: 1, action: DelegationAction::Decrease(50) },] + ); + assert_eq!( + state, + Delegator { + id: 1, + delegations: OrderedSet::from(vec![Bond { amount: 100, owner: 2 }]), + total: 100, + less_total: 0, + status: crate::DelegatorStatus::Active, + } + ); + } + + #[test] + fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() { + let mut state = Delegator { + id: 1, + delegations: OrderedSet::from(vec![Bond { amount: 100, owner: 2 }]), + total: 100, + less_total: 100, + status: crate::DelegatorStatus::Active, + }; + let mut scheduled_requests = + vec![ScheduledRequest { delegator: 2, when_executable: 1, action: DelegationAction::Decrease(50) }]; + let removed_request = >::cancel_request_with_state(&1, &mut state, &mut scheduled_requests); + + assert_eq!(removed_request, None,); + assert_eq!( + scheduled_requests, + vec![ScheduledRequest { delegator: 2, when_executable: 1, action: DelegationAction::Decrease(50) },] + ); + assert_eq!( + state, + Delegator { + id: 1, + delegations: OrderedSet::from(vec![Bond { amount: 100, owner: 2 }]), + total: 100, + less_total: 100, + status: crate::DelegatorStatus::Active, + } + ); + } +} diff --git a/pallets/parachain-staking/src/inflation.rs b/pallets/parachain-staking/src/inflation.rs new file mode 100644 index 0000000..c0e68e6 --- /dev/null +++ b/pallets/parachain-staking/src/inflation.rs @@ -0,0 +1,172 @@ +// 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 . + +//! Helper methods for computing issuance based on inflation +use crate::pallet::{BalanceOf, Config, Pallet}; +use frame_support::traits::fungible::Inspect; +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_runtime::{PerThing, Perbill, RuntimeDebug}; +use substrate_fixed::{transcendental::pow as floatpow, types::I64F64}; + +const SECONDS_PER_YEAR: u32 = 31557600; +const SECONDS_PER_BLOCK: u32 = 12; +pub const BLOCKS_PER_YEAR: u32 = SECONDS_PER_YEAR / SECONDS_PER_BLOCK; + +fn rounds_per_year() -> u32 { + let blocks_per_round = >::round().length; + BLOCKS_PER_YEAR / blocks_per_round +} + +#[derive( + Eq, PartialEq, Clone, Copy, Encode, Decode, Default, RuntimeDebug, MaxEncodedLen, TypeInfo, Serialize, Deserialize, +)] +pub struct Range { + pub min: T, + pub ideal: T, + pub max: T, +} + +impl Range { + pub fn is_valid(&self) -> bool { + self.max >= self.ideal && self.ideal >= self.min + } +} + +impl From for Range { + fn from(other: T) -> Range { + Range { min: other, ideal: other, max: other } + } +} +/// Convert an annual inflation to a round inflation +/// round = (1+annual)^(1/rounds_per_year) - 1 +pub fn perbill_annual_to_perbill_round(annual: Range, rounds_per_year: u32) -> Range { + let exponent = I64F64::from_num(1) / I64F64::from_num(rounds_per_year); + let annual_to_round = |annual: Perbill| -> Perbill { + let x = I64F64::from_num(annual.deconstruct()) / I64F64::from_num(Perbill::ACCURACY); + let y: I64F64 = floatpow(I64F64::from_num(1) + x, exponent) + .expect("Cannot overflow since rounds_per_year is u32 so worst case 0; QED"); + Perbill::from_parts(((y - I64F64::from_num(1)) * I64F64::from_num(Perbill::ACCURACY)).ceil().to_num::()) + }; + Range { min: annual_to_round(annual.min), ideal: annual_to_round(annual.ideal), max: annual_to_round(annual.max) } +} +/// Convert annual inflation rate range to round inflation range +pub fn annual_to_round(annual: Range) -> Range { + let periods = rounds_per_year::(); + perbill_annual_to_perbill_round(annual, periods) +} + +/// Compute round issuance range from round inflation range and current total issuance +pub fn round_issuance_range(round: Range) -> Range> { + let circulating = T::Currency::total_issuance(); + Range { min: round.min * circulating, ideal: round.ideal * circulating, max: round.max * circulating } +} + +#[derive(Eq, PartialEq, Clone, Encode, Decode, Default, RuntimeDebug, TypeInfo, Serialize, Deserialize)] +pub struct InflationInfo { + /// Staking expectations + pub expect: Range, + /// Annual inflation range + pub annual: Range, + /// Round inflation range + pub round: Range, +} + +impl InflationInfo { + pub fn new(annual: Range, expect: Range) -> InflationInfo { + InflationInfo { expect, annual, round: annual_to_round::(annual) } + } + + /// Set round inflation range according to input annual inflation range + pub fn set_round_from_annual(&mut self, new: Range) { + self.round = annual_to_round::(new); + } + + /// Reset round inflation rate based on changes to round length + pub fn reset_round(&mut self, new_length: u32) { + let periods = BLOCKS_PER_YEAR / new_length; + self.round = perbill_annual_to_perbill_round(self.annual, periods); + } + + /// Set staking expectations + pub fn set_expectations(&mut self, expect: Range) { + self.expect = expect; + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn mock_annual_to_round(annual: Range, rounds_per_year: u32) -> Range { + perbill_annual_to_perbill_round(annual, rounds_per_year) + } + fn mock_round_issuance_range( + // Total circulating before minting + circulating: u128, + // Round inflation range + round: Range, + ) -> Range { + Range { min: round.min * circulating, ideal: round.ideal * circulating, max: round.max * circulating } + } + #[test] + fn simple_issuance_conversion() { + // 5% inflation for 10_000_0000 = 500,000 minted over the year + // let's assume there are 10 periods in a year + // => mint 500_000 over 10 periods => 50_000 minted per period + let expected_round_issuance_range: Range = Range { min: 48_909, ideal: 48_909, max: 48_909 }; + let schedule = + Range { min: Perbill::from_percent(5), ideal: Perbill::from_percent(5), max: Perbill::from_percent(5) }; + assert_eq!( + expected_round_issuance_range, + mock_round_issuance_range(10_000_000, mock_annual_to_round(schedule, 10)) + ); + } + #[test] + fn range_issuance_conversion() { + // 3-5% inflation for 10_000_0000 = 300_000-500,000 minted over the year + // let's assume there are 10 periods in a year + // => mint 300_000-500_000 over 10 periods => 30_000-50_000 minted per period + let expected_round_issuance_range: Range = Range { min: 29_603, ideal: 39298, max: 48_909 }; + let schedule = + Range { min: Perbill::from_percent(3), ideal: Perbill::from_percent(4), max: Perbill::from_percent(5) }; + assert_eq!( + expected_round_issuance_range, + mock_round_issuance_range(10_000_000, mock_annual_to_round(schedule, 10)) + ); + } + #[test] + fn expected_parameterization() { + let expected_round_schedule: Range = Range { min: 45, ideal: 56, max: 56 }; + let schedule = + Range { min: Perbill::from_percent(4), ideal: Perbill::from_percent(5), max: Perbill::from_percent(5) }; + assert_eq!( + expected_round_schedule, + mock_round_issuance_range(10_000_000, mock_annual_to_round(schedule, 8766)) + ); + } + #[test] + fn inflation_does_not_panic_at_round_number_limit() { + let schedule = Range { + min: Perbill::from_percent(100), + ideal: Perbill::from_percent(100), + max: Perbill::from_percent(100), + }; + mock_round_issuance_range(u32::MAX.into(), mock_annual_to_round(schedule, u32::MAX)); + mock_round_issuance_range(u64::MAX.into(), mock_annual_to_round(schedule, u32::MAX)); + mock_round_issuance_range(u128::MAX.into(), mock_annual_to_round(schedule, u32::MAX)); + mock_round_issuance_range(u32::MAX.into(), mock_annual_to_round(schedule, 1)); + mock_round_issuance_range(u64::MAX.into(), mock_annual_to_round(schedule, 1)); + mock_round_issuance_range(u128::MAX.into(), mock_annual_to_round(schedule, 1)); + } +} diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs new file mode 100644 index 0000000..b1893bc --- /dev/null +++ b/pallets/parachain-staking/src/lib.rs @@ -0,0 +1,1950 @@ +// 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 . + +//! # Parachain Staking +//! Minimal staking pallet that implements collator selection by total backed stake. +//! The main difference between this pallet and `frame/pallet-staking` is that this pallet +//! uses direct delegation. Delegators choose exactly who they delegate and with what stake. +//! This is different from `frame/pallet-staking` where delegators approval vote and run Phragmen. +//! +//! ### Rules +//! There is a new round every `>::get().length` blocks. +//! +//! At the start of every round, +//! * issuance is calculated for collators (and their delegators) for block authoring +//! `T::RewardPaymentDelay` rounds ago +//! * a new set of collators is chosen from the candidates +//! +//! Immediately following a round change, payments are made once-per-block until all payments have +//! been made. In each such block, one collator is chosen for a rewards payment and is paid along +//! with each of its top `T::MaxTopDelegationsPerCandidate` delegators. +//! +//! To join the set of candidates, call `join_candidates` with `bond >= MinCandidateStk`. +//! To leave the set of candidates, call `schedule_leave_candidates`. If the call succeeds, +//! the collator is removed from the pool of candidates so they cannot be selected for future +//! collator sets, but they are not unbonded until their exit request is executed. Any signed +//! account may trigger the exit `T::LeaveCandidatesDelay` rounds after the round in which the +//! original request was made. +//! +//! To join the set of delegators, call `delegate` and pass in an account that is +//! already a collator candidate and `bond >= MinDelegatorStk`. Each delegator can delegate up to +//! `T::MaxDelegationsPerDelegator` collator candidates by calling `delegate`. +//! +//! To revoke a delegation, call `revoke_delegation` with the collator candidate's account. +//! To leave the set of delegators and revoke all delegations, call `leave_delegators`. + +#![cfg_attr(not(feature = "std"), no_std)] +// Needed due to empty sections raising the warning +#![allow(unreachable_patterns)] + +mod auto_compound; +mod delegation_requests; +pub mod inflation; +pub mod migrations; +pub mod traits; +pub mod types; +pub mod weights; + +#[cfg(any(test, feature = "runtime-benchmarks"))] +mod benchmarks; +#[cfg(test)] +mod mock; +mod set; +#[cfg(test)] +mod tests; + +use frame_support::pallet; +pub use inflation::{InflationInfo, Range}; +pub use weights::WeightInfo; + +pub use auto_compound::{AutoCompoundConfig, AutoCompoundDelegations}; +pub use delegation_requests::{CancelledScheduledRequest, DelegationAction, ScheduledRequest}; +pub use pallet::*; +pub use traits::*; +pub use types::*; +pub use RoundIndex; + +#[pallet] +pub mod pallet { + use crate::{ + delegation_requests::{CancelledScheduledRequest, DelegationAction, ScheduledRequest}, + set::OrderedSet, + traits::*, + types::*, + AutoCompoundConfig, AutoCompoundDelegations, InflationInfo, Range, WeightInfo, + }; + use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Inspect, InspectHold, Mutate, MutateHold}, + tokens::{Balance, Fortitude, Precision, Preservation}, + EstimateNextSessionRotation, Get, + }, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::{ + traits::{Saturating, Zero}, + Perbill, Percent, Permill, + }; + use sp_staking::SessionIndex; + use sp_std::{collections::btree_map::BTreeMap, prelude::*}; + + /// Pallet for parachain staking + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + pub type RoundIndex = u32; + type RewardPoint = u32; + pub type BalanceOf = ::Balance; + + pub type AccountIdOf = ::AccountId; + + /// A reason for the pallet parachain staking placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// + StakingCollator, + /// + StakingDelegator, + } + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Overarching hold reason. + type RuntimeHoldReason: From; + /// The currency type + type Currency: Inspect, Balance = BalanceOf> + + Balanced> + + Mutate> + + InspectHold> + + MutateHold, Reason = Self::RuntimeHoldReason>; + + type Balance: Balance + MaybeSerializeDeserialize; + /// The account that will pay the collator rewards + type PayMaster: Get; + /// The origin for monetary governance + type MonetaryGovernanceOrigin: EnsureOrigin; + /// Minimum number of blocks per round + #[pallet::constant] + type MinBlocksPerRound: Get; + /// Number of rounds that candidates remain bonded before exit request is executable + #[pallet::constant] + type LeaveCandidatesDelay: Get; + /// Number of rounds candidate requests to decrease self-bond must wait to be executable + #[pallet::constant] + type CandidateBondLessDelay: Get; + /// Number of rounds that delegators remain bonded before exit request is executable + #[pallet::constant] + type LeaveDelegatorsDelay: Get; + /// Number of rounds that delegations remain bonded before revocation request is executable + #[pallet::constant] + type RevokeDelegationDelay: Get; + /// Number of rounds that delegation less requests must wait before executable + #[pallet::constant] + type DelegationBondLessDelay: Get; + /// Number of rounds after which block authors are rewarded + #[pallet::constant] + type RewardPaymentDelay: Get; + /// Minimum number of selected candidates every round + #[pallet::constant] + type MinSelectedCandidates: Get; + /// Maximum top delegations counted per candidate + #[pallet::constant] + type MaxTopDelegationsPerCandidate: Get; + /// Maximum bottom delegations (not counted) per candidate + #[pallet::constant] + type MaxBottomDelegationsPerCandidate: Get; + /// Maximum delegations per delegator + #[pallet::constant] + type MaxDelegationsPerDelegator: Get; + /// Minimum stake required for any account to be a collator candidate + #[pallet::constant] + type MinCandidateStk: Get>; + /// Minimum stake for any registered on-chain account to delegate + #[pallet::constant] + type MinDelegation: Get>; + /// Minimum stake for any registered on-chain account to be a delegator + #[pallet::constant] + type MinDelegatorStk: Get>; + /// Handler to notify the runtime when a collator is paid. + /// If you don't need it, you can specify the type `()`. + type OnCollatorPayout: OnCollatorPayout>; + /// Handler to distribute a collator's reward. + /// To use the default implementation of minting rewards, specify the type `()`. + type PayoutCollatorReward: PayoutCollatorReward; + /// Handler to notify the runtime when a new round begin. + /// If you don't need it, you can specify the type `()`. + type OnNewRound: OnNewRound; + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::error] + pub enum Error { + DelegatorDNE, + DelegatorDNEinTopNorBottom, + DelegatorDNEInDelegatorSet, + CandidateDNE, + DelegationDNE, + DelegatorExists, + CandidateExists, + CandidateBondBelowMin, + InsufficientBalance, + DelegatorBondBelowMin, + DelegationBelowMin, + AlreadyOffline, + AlreadyActive, + DelegatorAlreadyLeaving, + DelegatorNotLeaving, + DelegatorCannotLeaveYet, + CannotDelegateIfLeaving, + CandidateAlreadyLeaving, + CandidateNotLeaving, + CandidateCannotLeaveYet, + CannotGoOnlineIfLeaving, + ExceedMaxDelegationsPerDelegator, + AlreadyDelegatedCandidate, + InvalidSchedule, + CannotSetBelowMin, + RoundLengthMustBeGreaterThanTotalSelectedCollators, + NoWritingSameValue, + TooLowCandidateCountWeightHintJoinCandidates, + TooLowCandidateCountWeightHintCancelLeaveCandidates, + TooLowCandidateCountToLeaveCandidates, + TooLowDelegationCountToDelegate, + TooLowCandidateDelegationCountToDelegate, + TooLowCandidateDelegationCountToLeaveCandidates, + TooLowDelegationCountToLeaveDelegators, + PendingCandidateRequestsDNE, + PendingCandidateRequestAlreadyExists, + PendingCandidateRequestNotDueYet, + PendingDelegationRequestDNE, + PendingDelegationRequestAlreadyExists, + PendingDelegationRequestNotDueYet, + CannotDelegateLessThanOrEqualToLowestBottomWhenFull, + PendingDelegationRevoke, + TooLowDelegationCountToAutoCompound, + TooLowCandidateAutoCompoundingDelegationCountToAutoCompound, + TooLowCandidateAutoCompoundingDelegationCountToDelegate, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Started new round. + NewRound { + starting_block: BlockNumberFor, + round: RoundIndex, + selected_collators_number: u32, + total_balance: BalanceOf, + }, + /// Account joined the set of collator candidates. + JoinedCollatorCandidates { + account: T::AccountId, + amount_locked: BalanceOf, + new_total_amt_locked: BalanceOf, + }, + /// Candidate selected for collators. Total Exposed Amount includes all delegations. + CollatorChosen { + round: RoundIndex, + collator_account: T::AccountId, + total_exposed_amount: BalanceOf, + }, + /// Candidate requested to decrease a self bond. + CandidateBondLessRequested { + candidate: T::AccountId, + amount_to_decrease: BalanceOf, + execute_round: RoundIndex, + }, + /// Candidate has increased a self bond. + CandidateBondedMore { + candidate: T::AccountId, + amount: BalanceOf, + new_total_bond: BalanceOf, + }, + /// Candidate has decreased a self bond. + CandidateBondedLess { + candidate: T::AccountId, + amount: BalanceOf, + new_bond: BalanceOf, + }, + /// Candidate temporarily leave the set of collator candidates without unbonding. + CandidateWentOffline { + candidate: T::AccountId, + }, + /// Candidate rejoins the set of collator candidates. + CandidateBackOnline { + candidate: T::AccountId, + }, + /// Candidate has requested to leave the set of candidates. + CandidateScheduledExit { + exit_allowed_round: RoundIndex, + candidate: T::AccountId, + scheduled_exit: RoundIndex, + }, + /// Cancelled request to leave the set of candidates. + CancelledCandidateExit { + candidate: T::AccountId, + }, + /// Cancelled request to decrease candidate's bond. + CancelledCandidateBondLess { + candidate: T::AccountId, + amount: BalanceOf, + execute_round: RoundIndex, + }, + /// Candidate has left the set of candidates. + CandidateLeft { + ex_candidate: T::AccountId, + unlocked_amount: BalanceOf, + new_total_amt_locked: BalanceOf, + }, + /// Delegator requested to decrease a bond for the collator candidate. + DelegationDecreaseScheduled { + delegator: T::AccountId, + candidate: T::AccountId, + amount_to_decrease: BalanceOf, + execute_round: RoundIndex, + }, + // Delegation increased. + DelegationIncreased { + delegator: T::AccountId, + candidate: T::AccountId, + amount: BalanceOf, + in_top: bool, + }, + // Delegation decreased. + DelegationDecreased { + delegator: T::AccountId, + candidate: T::AccountId, + amount: BalanceOf, + in_top: bool, + }, + /// Delegator requested to leave the set of delegators. + DelegatorExitScheduled { + round: RoundIndex, + delegator: T::AccountId, + scheduled_exit: RoundIndex, + }, + /// Delegator requested to revoke delegation. + DelegationRevocationScheduled { + round: RoundIndex, + delegator: T::AccountId, + candidate: T::AccountId, + scheduled_exit: RoundIndex, + }, + /// Delegator has left the set of delegators. + DelegatorLeft { + delegator: T::AccountId, + unstaked_amount: BalanceOf, + }, + /// Delegation revoked. + DelegationRevoked { + delegator: T::AccountId, + candidate: T::AccountId, + unstaked_amount: BalanceOf, + }, + /// Delegation kicked. + DelegationKicked { + delegator: T::AccountId, + candidate: T::AccountId, + unstaked_amount: BalanceOf, + }, + /// Cancelled a pending request to exit the set of delegators. + DelegatorExitCancelled { + delegator: T::AccountId, + }, + /// Cancelled request to change an existing delegation. + CancelledDelegationRequest { + delegator: T::AccountId, + cancelled_request: CancelledScheduledRequest>, + collator: T::AccountId, + }, + /// New delegation (increase of the existing one). + Delegation { + delegator: T::AccountId, + locked_amount: BalanceOf, + candidate: T::AccountId, + delegator_position: DelegatorAdded>, + auto_compound: Percent, + }, + /// Delegation from candidate state has been remove. + DelegatorLeftCandidate { + delegator: T::AccountId, + candidate: T::AccountId, + unstaked_amount: BalanceOf, + total_candidate_staked: BalanceOf, + }, + /// Paid the account (delegator or collator) the balance as liquid rewards. + Rewarded { + account: T::AccountId, + rewards: BalanceOf, + }, + /// Transferred to account which holds funds reserved for parachain bond. + ReservedForParachainBond { + account: T::AccountId, + value: BalanceOf, + }, + /// Account (re)set for parachain bond treasury. + ParachainBondAccountSet { + old: T::AccountId, + new: T::AccountId, + }, + /// Percent of inflation reserved for parachain bond (re)set. + ParachainBondReservePercentSet { + old: Percent, + new: Percent, + }, + /// Annual inflation input (first 3) was used to derive new per-round inflation (last 3) + InflationSet { + annual_min: Perbill, + annual_ideal: Perbill, + annual_max: Perbill, + round_min: Perbill, + round_ideal: Perbill, + round_max: Perbill, + }, + /// Staking expectations set. + StakeExpectationsSet { + expect_min: BalanceOf, + expect_ideal: BalanceOf, + expect_max: BalanceOf, + }, + /// Set total selected candidates to this value. + TotalSelectedSet { + old: u32, + new: u32, + }, + /// Set collator commission to this value. + CollatorCommissionSet { + old: Perbill, + new: Perbill, + }, + /// Set blocks per round + BlocksPerRoundSet { + current_round: RoundIndex, + first_block: BlockNumberFor, + old: u32, + new: u32, + new_per_round_inflation_min: Perbill, + new_per_round_inflation_ideal: Perbill, + new_per_round_inflation_max: Perbill, + }, + /// Auto-compounding reward percent was set for a delegation. + AutoCompoundSet { + candidate: T::AccountId, + delegator: T::AccountId, + value: Percent, + }, + /// Compounded a portion of rewards towards the delegation. + Compounded { + candidate: T::AccountId, + delegator: T::AccountId, + amount: BalanceOf, + }, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + let mut weight = T::WeightInfo::base_on_initialize(); + + let mut round = >::get(); + + if round.should_update(n) { + // mutate round + round.update(n); + // notify that new round begin + weight = weight.saturating_add(T::OnNewRound::on_new_round(round.current)); + // pay all stakers for T::RewardPaymentDelay rounds ago + weight = weight.saturating_add(Self::prepare_staking_payouts(round.current)); + // select top collator candidates for next round + let (extra_weight, collator_count, _delegation_count, total_staked) = + Self::select_top_candidates(round.current); + weight = weight.saturating_add(extra_weight); + // start next round + >::put(round); + // snapshot total stake + >::insert(round.current, >::get()); + Self::deposit_event(Event::NewRound { + starting_block: round.first, + round: round.current, + selected_collators_number: collator_count, + total_balance: total_staked, + }); + // account for Round and Staked writes + weight = weight.saturating_add(T::DbWeight::get().reads_writes(0, 2)); + } else { + weight = weight.saturating_add(Self::handle_delayed_payouts(round.current)); + } + + // add on_finalize weight + // read: Author, Points, AwardedPts + // write: Points, AwardedPts + weight = weight.saturating_add(T::DbWeight::get().reads_writes(3, 2)); + weight + } + } + + #[pallet::storage] + #[pallet::getter(fn collator_commission)] + /// Commission percent taken off of rewards for all collators + type CollatorCommission = StorageValue<_, Perbill, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn total_selected)] + /// The total candidates selected every round + pub(crate) type TotalSelected = StorageValue<_, u32, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn parachain_bond_info)] + /// Parachain bond config info { account, percent_of_inflation } + pub(crate) type ParachainBondInfo = StorageValue<_, ParachainBondConfig, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn round)] + /// Current round index and next round scheduled transition + pub(crate) type Round = StorageValue<_, RoundInfo>, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn delegator_state)] + /// Get delegator state associated with an account if account is delegating else None + pub(crate) type DelegatorState = + StorageMap<_, Twox64Concat, T::AccountId, Delegator>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn candidate_info)] + /// Get collator candidate info associated with an account if account is candidate else None + pub(crate) type CandidateInfo = + StorageMap<_, Twox64Concat, T::AccountId, CandidateMetadata>, OptionQuery>; + + /// Stores outstanding delegation requests per collator. + #[pallet::storage] + #[pallet::getter(fn delegation_scheduled_requests)] + pub(crate) type DelegationScheduledRequests = + StorageMap<_, Blake2_128Concat, T::AccountId, Vec>>, ValueQuery>; + + pub struct AddGet { + _phantom: PhantomData<(T, R)>, + } + impl Get for AddGet + where + T: Get, + R: Get, + { + fn get() -> u32 { + T::get() + R::get() + } + } + + /// Stores auto-compounding configuration per collator. + #[pallet::storage] + #[pallet::getter(fn auto_compounding_delegations)] + pub(crate) type AutoCompoundingDelegations = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec< + AutoCompoundConfig, + AddGet, + >, + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn top_delegations)] + /// Top delegations for collator candidate + pub(crate) type TopDelegations = + StorageMap<_, Twox64Concat, T::AccountId, Delegations>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn bottom_delegations)] + /// Bottom delegations for collator candidate + pub(crate) type BottomDelegations = + StorageMap<_, Twox64Concat, T::AccountId, Delegations>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn selected_candidates)] + /// The collator candidates selected for the current round + type SelectedCandidates = StorageValue<_, Vec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn total)] + /// Total capital locked by this staking pallet + pub(crate) type Total = StorageValue<_, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn candidate_pool)] + /// The pool of collator candidates, each with their total backing stake + pub(crate) type CandidatePool = + StorageValue<_, OrderedSet>>, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn at_stake)] + /// Snapshot of collator delegation stake at the start of the round + pub type AtStake = StorageDoubleMap< + _, + Twox64Concat, + RoundIndex, + Twox64Concat, + T::AccountId, + CollatorSnapshot>, + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn delayed_payouts)] + /// Delayed payouts + pub type DelayedPayouts = + StorageMap<_, Twox64Concat, RoundIndex, DelayedPayout>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn staked)] + /// Total counted stake for selected candidates in the round + pub type Staked = StorageMap<_, Twox64Concat, RoundIndex, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn inflation_config)] + /// Inflation configuration + pub type InflationConfig = StorageValue<_, InflationInfo>, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn points)] + /// Total points awarded to collators for block production in the round + pub type Points = StorageMap<_, Twox64Concat, RoundIndex, RewardPoint, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn awarded_pts)] + /// Points for each collator per round + pub type AwardedPts = + StorageDoubleMap<_, Twox64Concat, RoundIndex, Twox64Concat, T::AccountId, RewardPoint, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + /// Initialize balance and register all as collators: `(collator AccountId, balance Amount)` + pub candidates: Vec<(T::AccountId, BalanceOf)>, + /// Initialize balance and make delegations: + /// `(delegator AccountId, collator AccountId, delegation Amount, auto-compounding Percent)` + pub delegations: Vec<(T::AccountId, T::AccountId, BalanceOf, Percent)>, + /// Inflation configuration + pub inflation_config: InflationInfo>, + /// Default fixed percent a collator takes off the top of due rewards + pub collator_commission: Perbill, + /// Default percent of inflation set aside for parachain bond every round + pub parachain_bond_reserve_percent: Percent, + /// Default number of blocks in a round + pub blocks_per_round: u32, + /// Number of selected candidates every round. Cannot be lower than MinSelectedCandidates + pub num_selected_candidates: u32, + } + + impl Default for GenesisConfig { + fn default() -> Self { + Self { + candidates: vec![], + delegations: vec![], + inflation_config: Default::default(), + collator_commission: Default::default(), + parachain_bond_reserve_percent: Default::default(), + blocks_per_round: 1u32, + num_selected_candidates: T::MinSelectedCandidates::get(), + } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + assert!(self.blocks_per_round > 0, "Blocks per round must be > 0"); + >::put(self.inflation_config.clone()); + let mut candidate_count = 0u32; + // Initialize the candidates + for &(ref candidate, balance) in &self.candidates { + assert!( + >::get_collator_stakable_free_balance(candidate) >= balance, + "Account does not have enough balance to bond as a candidate." + ); + if let Err(error) = >::join_candidates( + T::RuntimeOrigin::from(Some(candidate.clone()).into()), + balance, + candidate_count, + ) { + log::warn!("Join candidates failed in genesis with error {:?}", error); + } else { + candidate_count = candidate_count.saturating_add(1u32); + } + } + + let mut col_delegator_count: BTreeMap = BTreeMap::new(); + let mut col_auto_compound_delegator_count: BTreeMap = BTreeMap::new(); + let mut del_delegation_count: BTreeMap = BTreeMap::new(); + // Initialize the delegations + for &(ref delegator, ref target, balance, auto_compound) in &self.delegations { + assert!( + >::get_delegator_stakable_free_balance(delegator) >= balance, + "Account does not have enough balance to place delegation." + ); + let cd_count = if let Some(x) = col_delegator_count.get(target) { *x } else { 0u32 }; + let dd_count = if let Some(x) = del_delegation_count.get(delegator) { *x } else { 0u32 }; + let cd_auto_compound_count = col_auto_compound_delegator_count.get(target).cloned().unwrap_or_default(); + if let Err(error) = >::delegate_with_auto_compound( + T::RuntimeOrigin::from(Some(delegator.clone()).into()), + target.clone(), + balance, + auto_compound, + cd_count, + cd_auto_compound_count, + dd_count, + ) { + log::warn!("Delegate failed in genesis with error {:?}", error); + } else { + if let Some(x) = col_delegator_count.get_mut(target) { + *x = x.saturating_add(1u32); + } else { + col_delegator_count.insert(target.clone(), 1u32); + }; + if let Some(x) = del_delegation_count.get_mut(delegator) { + *x = x.saturating_add(1u32); + } else { + del_delegation_count.insert(delegator.clone(), 1u32); + }; + if !auto_compound.is_zero() { + col_auto_compound_delegator_count + .entry(target.clone()) + .and_modify(|x| *x = x.saturating_add(1)) + .or_insert(1); + } + } + } + // Set collator commission to default config + >::put(self.collator_commission); + // Set parachain bond config to default config + >::put(ParachainBondConfig { + // must be set soon; if not => due inflation will be sent to collators/delegators + account: T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"), + percent: self.parachain_bond_reserve_percent, + }); + // Set total selected candidates to value from config + assert!( + self.num_selected_candidates >= T::MinSelectedCandidates::get(), + "{:?}", + Error::::CannotSetBelowMin + ); + >::put(self.num_selected_candidates); + // Choose top TotalSelected collator candidates + let (_, v_count, _, total_staked) = >::select_top_candidates(1u32); + // Start Round 1 at Block 0 + let round: RoundInfo> = RoundInfo::new(1u32, 0u32.into(), self.blocks_per_round); + >::put(round); + // Snapshot total stake + >::insert(1u32, >::get()); + >::deposit_event(Event::NewRound { + starting_block: BlockNumberFor::::zero(), + round: 1u32, + selected_collators_number: v_count, + total_balance: total_staked, + }); + } + } + + #[pallet::call] + impl Pallet { + /// Set the expectations for total staked. These expectations determine the issuance for + /// the round according to logic in `fn compute_issuance` + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::set_staking_expectations())] + pub fn set_staking_expectations( + origin: OriginFor, + expectations: Range>, + ) -> DispatchResultWithPostInfo { + T::MonetaryGovernanceOrigin::ensure_origin(origin)?; + ensure!(expectations.is_valid(), Error::::InvalidSchedule); + let mut config = >::get(); + ensure!(config.expect != expectations, Error::::NoWritingSameValue); + config.set_expectations(expectations); + Self::deposit_event(Event::StakeExpectationsSet { + expect_min: config.expect.min, + expect_ideal: config.expect.ideal, + expect_max: config.expect.max, + }); + >::put(config); + Ok(().into()) + } + + /// Set the annual inflation rate to derive per-round inflation + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::set_inflation())] + pub fn set_inflation(origin: OriginFor, schedule: Range) -> DispatchResultWithPostInfo { + T::MonetaryGovernanceOrigin::ensure_origin(origin)?; + ensure!(schedule.is_valid(), Error::::InvalidSchedule); + let mut config = >::get(); + ensure!(config.annual != schedule, Error::::NoWritingSameValue); + config.annual = schedule; + config.set_round_from_annual::(schedule); + Self::deposit_event(Event::InflationSet { + annual_min: config.annual.min, + annual_ideal: config.annual.ideal, + annual_max: config.annual.max, + round_min: config.round.min, + round_ideal: config.round.ideal, + round_max: config.round.max, + }); + >::put(config); + Ok(().into()) + } + + /// Set the account that will hold funds set aside for parachain bond + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::set_parachain_bond_account())] + pub fn set_parachain_bond_account(origin: OriginFor, new: T::AccountId) -> DispatchResultWithPostInfo { + T::MonetaryGovernanceOrigin::ensure_origin(origin)?; + let ParachainBondConfig { account: old, percent } = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); + >::put(ParachainBondConfig { account: new.clone(), percent }); + Self::deposit_event(Event::ParachainBondAccountSet { old, new }); + Ok(().into()) + } + + /// Set the percent of inflation set aside for parachain bond + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::set_parachain_bond_reserve_percent())] + pub fn set_parachain_bond_reserve_percent(origin: OriginFor, new: Percent) -> DispatchResultWithPostInfo { + T::MonetaryGovernanceOrigin::ensure_origin(origin)?; + let ParachainBondConfig { account, percent: old } = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); + >::put(ParachainBondConfig { account, percent: new }); + Self::deposit_event(Event::ParachainBondReservePercentSet { old, new }); + Ok(().into()) + } + + /// Set the total number of collator candidates selected per round + /// - changes are not applied until the start of the next round + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::set_total_selected())] + pub fn set_total_selected(origin: OriginFor, new: u32) -> DispatchResultWithPostInfo { + frame_system::ensure_root(origin)?; + ensure!(new >= T::MinSelectedCandidates::get(), Error::::CannotSetBelowMin); + let old = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); + ensure!(new < >::get().length, Error::::RoundLengthMustBeGreaterThanTotalSelectedCollators,); + >::put(new); + Self::deposit_event(Event::TotalSelectedSet { old, new }); + Ok(().into()) + } + + /// Set the commission for all collators + #[pallet::call_index(5)] + #[pallet::weight(::WeightInfo::set_collator_commission())] + pub fn set_collator_commission(origin: OriginFor, new: Perbill) -> DispatchResultWithPostInfo { + frame_system::ensure_root(origin)?; + let old = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); + >::put(new); + Self::deposit_event(Event::CollatorCommissionSet { old, new }); + Ok(().into()) + } + + /// Set blocks per round + /// - if called with `new` less than length of current round, will transition immediately + /// in the next block + /// - also updates per-round inflation config + #[pallet::call_index(6)] + #[pallet::weight(::WeightInfo::set_blocks_per_round())] + pub fn set_blocks_per_round(origin: OriginFor, new: u32) -> DispatchResultWithPostInfo { + frame_system::ensure_root(origin)?; + ensure!(new >= T::MinBlocksPerRound::get(), Error::::CannotSetBelowMin); + let mut round = >::get(); + let (now, first, old) = (round.current, round.first, round.length); + ensure!(old != new, Error::::NoWritingSameValue); + ensure!(new > >::get(), Error::::RoundLengthMustBeGreaterThanTotalSelectedCollators,); + round.length = new; + // update per-round inflation given new rounds per year + let mut inflation_config = >::get(); + inflation_config.reset_round(new); + >::put(round); + Self::deposit_event(Event::BlocksPerRoundSet { + current_round: now, + first_block: first, + old, + new, + new_per_round_inflation_min: inflation_config.round.min, + new_per_round_inflation_ideal: inflation_config.round.ideal, + new_per_round_inflation_max: inflation_config.round.max, + }); + >::put(inflation_config); + Ok(().into()) + } + + /// Join the set of collator candidates + #[pallet::call_index(7)] + #[pallet::weight(::WeightInfo::join_candidates(*candidate_count))] + pub fn join_candidates( + origin: OriginFor, + bond: BalanceOf, + candidate_count: u32, + ) -> DispatchResultWithPostInfo { + let acc = ensure_signed(origin)?; + ensure!(!Self::is_candidate(&acc), Error::::CandidateExists); + ensure!(!Self::is_delegator(&acc), Error::::DelegatorExists); + ensure!(bond >= T::MinCandidateStk::get(), Error::::CandidateBondBelowMin); + let mut candidates = >::get(); + let old_count = candidates.0.len() as u32; + ensure!(candidate_count >= old_count, Error::::TooLowCandidateCountWeightHintJoinCandidates); + ensure!(candidates.insert(Bond { owner: acc.clone(), amount: bond }), Error::::CandidateExists); + ensure!(Self::get_collator_stakable_free_balance(&acc) >= bond, Error::::InsufficientBalance,); + T::Currency::hold(&HoldReason::StakingCollator.into(), &acc, bond)?; + let candidate = CandidateMetadata::new(bond); + >::insert(&acc, candidate); + let empty_delegations: Delegations> = Default::default(); + // insert empty top delegations + >::insert(&acc, empty_delegations.clone()); + // insert empty bottom delegations + >::insert(&acc, empty_delegations); + >::put(candidates); + let new_total = >::get().saturating_add(bond); + >::put(new_total); + Self::deposit_event(Event::JoinedCollatorCandidates { + account: acc, + amount_locked: bond, + new_total_amt_locked: new_total, + }); + Ok(().into()) + } + + /// Request to leave the set of candidates. If successful, the account is immediately + /// removed from the candidate pool to prevent selection as a collator. + #[pallet::call_index(8)] + #[pallet::weight(::WeightInfo::schedule_leave_candidates(*candidate_count))] + pub fn schedule_leave_candidates(origin: OriginFor, candidate_count: u32) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + let (now, when) = state.schedule_leave::()?; + let mut candidates = >::get(); + ensure!(candidate_count >= candidates.0.len() as u32, Error::::TooLowCandidateCountToLeaveCandidates); + if candidates.remove(&Bond::from_owner(collator.clone())) { + >::put(candidates); + } + >::insert(&collator, state); + Self::deposit_event(Event::CandidateScheduledExit { + exit_allowed_round: now, + candidate: collator, + scheduled_exit: when, + }); + Ok(().into()) + } + + /// Execute leave candidates request + #[pallet::call_index(9)] + #[pallet::weight( + ::WeightInfo::execute_leave_candidates(*candidate_delegation_count) + )] + pub fn execute_leave_candidates( + origin: OriginFor, + candidate: T::AccountId, + candidate_delegation_count: u32, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + let state = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + ensure!( + state.delegation_count <= candidate_delegation_count, + Error::::TooLowCandidateDelegationCountToLeaveCandidates + ); + state.can_leave::()?; + let return_stake = |bond: Bond>| -> DispatchResult { + // remove delegation from delegator state + let mut delegator = DelegatorState::::get(&bond.owner).expect( + "Collator state and delegator state are consistent. + Collator state has a record of this delegation. Therefore, + Delegator state also has a record. qed.", + ); + + if let Some(remaining) = delegator.rm_delegation::(&candidate) { + Self::delegation_remove_request_with_state(&candidate, &bond.owner, &mut delegator); + >::remove_auto_compound(&candidate, &bond.owner); + + if remaining.is_zero() { + // we do not remove the scheduled delegation requests from other collators + // since it is assumed that they were removed incrementally before only the + // last delegation was left. + >::remove(&bond.owner); + let total_bonded = + T::Currency::balance_on_hold(&HoldReason::StakingDelegator.into(), &bond.owner); + T::Currency::release( + &HoldReason::StakingDelegator.into(), + &bond.owner, + total_bonded, + Precision::Exact, + )?; + } else { + >::insert(&bond.owner, delegator); + } + } else { + // TODO: review. we assume here that this delegator has no remaining staked + // balance, so we ensure the lock is cleared + let total_bonded = T::Currency::balance_on_hold(&HoldReason::StakingDelegator.into(), &bond.owner); + T::Currency::release( + &HoldReason::StakingDelegator.into(), + &bond.owner, + total_bonded, + Precision::Exact, + )?; + } + Ok(()) + }; + // total backing stake is at least the candidate self bond + let mut total_backing = state.bond; + // return all top delegations + let top_delegations = >::take(&candidate).expect("CandidateInfo existence checked"); + for bond in top_delegations.delegations { + return_stake(bond)?; + } + total_backing = total_backing.saturating_add(top_delegations.total); + // return all bottom delegations + let bottom_delegations = >::take(&candidate).expect("CandidateInfo existence checked"); + for bond in bottom_delegations.delegations { + return_stake(bond)?; + } + total_backing = total_backing.saturating_add(bottom_delegations.total); + // return stake to collator + let total_bonded = T::Currency::balance_on_hold(&HoldReason::StakingCollator.into(), &candidate); + T::Currency::release(&HoldReason::StakingCollator.into(), &candidate, total_bonded, Precision::Exact)?; + >::remove(&candidate); + >::remove(&candidate); + >::remove(&candidate); + >::remove(&candidate); + >::remove(&candidate); + let new_total_staked = >::get().saturating_sub(total_backing); + >::put(new_total_staked); + Self::deposit_event(Event::CandidateLeft { + ex_candidate: candidate, + unlocked_amount: total_backing, + new_total_amt_locked: new_total_staked, + }); + Ok(().into()) + } + + /// Cancel open request to leave candidates + /// - only callable by collator account + /// - result upon successful call is the candidate is active in the candidate pool + #[pallet::call_index(10)] + #[pallet::weight(::WeightInfo::cancel_leave_candidates(*candidate_count))] + pub fn cancel_leave_candidates(origin: OriginFor, candidate_count: u32) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + ensure!(state.is_leaving(), Error::::CandidateNotLeaving); + state.go_online(); + let mut candidates = >::get(); + ensure!( + candidates.0.len() as u32 <= candidate_count, + Error::::TooLowCandidateCountWeightHintCancelLeaveCandidates + ); + ensure!( + candidates.insert(Bond { owner: collator.clone(), amount: state.total_counted }), + Error::::AlreadyActive + ); + >::put(candidates); + >::insert(&collator, state); + Self::deposit_event(Event::CancelledCandidateExit { candidate: collator }); + Ok(().into()) + } + + /// Temporarily leave the set of collator candidates without unbonding + #[pallet::call_index(11)] + #[pallet::weight(::WeightInfo::go_offline())] + pub fn go_offline(origin: OriginFor) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + ensure!(state.is_active(), Error::::AlreadyOffline); + state.go_offline(); + let mut candidates = >::get(); + if candidates.remove(&Bond::from_owner(collator.clone())) { + >::put(candidates); + } + >::insert(&collator, state); + Self::deposit_event(Event::CandidateWentOffline { candidate: collator }); + Ok(().into()) + } + + /// Rejoin the set of collator candidates if previously had called `go_offline` + #[pallet::call_index(12)] + #[pallet::weight(::WeightInfo::go_online())] + pub fn go_online(origin: OriginFor) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + ensure!(!state.is_active(), Error::::AlreadyActive); + ensure!(!state.is_leaving(), Error::::CannotGoOnlineIfLeaving); + state.go_online(); + let mut candidates = >::get(); + ensure!( + candidates.insert(Bond { owner: collator.clone(), amount: state.total_counted }), + Error::::AlreadyActive + ); + >::put(candidates); + >::insert(&collator, state); + Self::deposit_event(Event::CandidateBackOnline { candidate: collator }); + Ok(().into()) + } + + /// Increase collator candidate self bond by `more` + #[pallet::call_index(13)] + #[pallet::weight(::WeightInfo::candidate_bond_more())] + pub fn candidate_bond_more(origin: OriginFor, more: BalanceOf) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + state.bond_more::(collator.clone(), more)?; + let (is_active, total_counted) = (state.is_active(), state.total_counted); + >::insert(&collator, state); + if is_active { + Self::update_active(collator, total_counted); + } + Ok(().into()) + } + + /// Request by collator candidate to decrease self bond by `less` + #[pallet::call_index(14)] + #[pallet::weight(::WeightInfo::schedule_candidate_bond_less())] + pub fn schedule_candidate_bond_less(origin: OriginFor, less: BalanceOf) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + let when = state.schedule_bond_less::(less)?; + >::insert(&collator, state); + Self::deposit_event(Event::CandidateBondLessRequested { + candidate: collator, + amount_to_decrease: less, + execute_round: when, + }); + Ok(().into()) + } + + /// Execute pending request to adjust the collator candidate self bond + #[pallet::call_index(15)] + #[pallet::weight(::WeightInfo::execute_candidate_bond_less())] + pub fn execute_candidate_bond_less( + origin: OriginFor, + candidate: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; // we may want to reward this if caller != candidate + let mut state = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + state.execute_bond_less::(candidate.clone())?; + >::insert(&candidate, state); + Ok(().into()) + } + + /// Cancel pending request to adjust the collator candidate self bond + #[pallet::call_index(16)] + #[pallet::weight(::WeightInfo::cancel_candidate_bond_less())] + pub fn cancel_candidate_bond_less(origin: OriginFor) -> DispatchResultWithPostInfo { + let collator = ensure_signed(origin)?; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + state.cancel_bond_less::(collator.clone())?; + >::insert(&collator, state); + Ok(().into()) + } + + /// If caller is not a delegator and not a collator, then join the set of delegators + /// If caller is a delegator, then makes delegation to change their delegation state + #[pallet::call_index(17)] + #[pallet::weight( + ::WeightInfo::delegate( + *candidate_delegation_count, + *delegation_count + ) + )] + pub fn delegate( + origin: OriginFor, + candidate: T::AccountId, + amount: BalanceOf, + candidate_delegation_count: u32, + delegation_count: u32, + ) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + >::delegate_with_auto_compound( + candidate, + delegator, + amount, + Percent::zero(), + candidate_delegation_count, + 0, + delegation_count, + ) + } + + /// If caller is not a delegator and not a collator, then join the set of delegators + /// If caller is a delegator, then makes delegation to change their delegation state + /// Sets the auto-compound config for the delegation + #[pallet::call_index(18)] + #[pallet::weight( + ::WeightInfo::delegate_with_auto_compound( + *candidate_delegation_count, + *candidate_auto_compounding_delegation_count, + *delegation_count, + ) + )] + pub fn delegate_with_auto_compound( + origin: OriginFor, + candidate: T::AccountId, + amount: BalanceOf, + auto_compound: Percent, + candidate_delegation_count: u32, + candidate_auto_compounding_delegation_count: u32, + delegation_count: u32, + ) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + >::delegate_with_auto_compound( + candidate, + delegator, + amount, + auto_compound, + candidate_delegation_count, + candidate_auto_compounding_delegation_count, + delegation_count, + ) + } + + /// DEPRECATED use batch util with schedule_revoke_delegation for all delegations + /// Request to leave the set of delegators. If successful, the caller is scheduled to be + /// allowed to exit via a [DelegationAction::Revoke] towards all existing delegations. + /// Success forbids future delegation requests until the request is invoked or cancelled. + #[pallet::call_index(19)] + #[pallet::weight(::WeightInfo::schedule_leave_delegators())] + pub fn schedule_leave_delegators(origin: OriginFor) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + Self::delegator_schedule_revoke_all(delegator) + } + + /// DEPRECATED use batch util with execute_delegation_request for all delegations + /// Execute the right to exit the set of delegators and revoke all ongoing delegations. + #[pallet::call_index(20)] + #[pallet::weight(::WeightInfo::execute_leave_delegators(*delegation_count))] + pub fn execute_leave_delegators( + origin: OriginFor, + delegator: T::AccountId, + delegation_count: u32, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + Self::delegator_execute_scheduled_revoke_all(delegator, delegation_count) + } + + /// DEPRECATED use batch util with cancel_delegation_request for all delegations + /// Cancel a pending request to exit the set of delegators. Success clears the pending exit + /// request (thereby resetting the delay upon another `leave_delegators` call). + #[pallet::call_index(21)] + #[pallet::weight(::WeightInfo::cancel_leave_delegators())] + pub fn cancel_leave_delegators(origin: OriginFor) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + Self::delegator_cancel_scheduled_revoke_all(delegator) + } + + /// Request to revoke an existing delegation. If successful, the delegation is scheduled + /// to be allowed to be revoked via the `execute_delegation_request` extrinsic. + /// The delegation receives no rewards for the rounds while a revoke is pending. + /// A revoke may not be performed if any other scheduled request is pending. + #[pallet::call_index(22)] + #[pallet::weight(::WeightInfo::schedule_revoke_delegation())] + pub fn schedule_revoke_delegation(origin: OriginFor, collator: T::AccountId) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + Self::delegation_schedule_revoke(collator, delegator) + } + + /// Bond more for delegators wrt a specific collator candidate. + #[pallet::call_index(23)] + #[pallet::weight(::WeightInfo::delegator_bond_more())] + pub fn delegator_bond_more( + origin: OriginFor, + candidate: T::AccountId, + more: BalanceOf, + ) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + let in_top = Self::delegation_bond_more_without_event(delegator.clone(), candidate.clone(), more)?; + Pallet::::deposit_event(Event::DelegationIncreased { delegator, candidate, amount: more, in_top }); + + Ok(().into()) + } + + /// Request bond less for delegators wrt a specific collator candidate. The delegation's + /// rewards for rounds while the request is pending use the reduced bonded amount. + /// A bond less may not be performed if any other scheduled request is pending. + #[pallet::call_index(24)] + #[pallet::weight(::WeightInfo::schedule_delegator_bond_less())] + pub fn schedule_delegator_bond_less( + origin: OriginFor, + candidate: T::AccountId, + less: BalanceOf, + ) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + Self::delegation_schedule_bond_decrease(candidate, delegator, less) + } + + /// Execute pending request to change an existing delegation + #[pallet::call_index(25)] + #[pallet::weight(::WeightInfo::execute_delegator_bond_less())] + pub fn execute_delegation_request( + origin: OriginFor, + delegator: T::AccountId, + candidate: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; // we may want to reward caller if caller != delegator + Self::delegation_execute_scheduled_request(candidate, delegator) + } + + /// Cancel request to change an existing delegation. + #[pallet::call_index(26)] + #[pallet::weight(::WeightInfo::cancel_delegator_bond_less())] + pub fn cancel_delegation_request(origin: OriginFor, candidate: T::AccountId) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + Self::delegation_cancel_request(candidate, delegator) + } + + /// Sets the auto-compounding reward percentage for a delegation. + #[pallet::call_index(27)] + #[pallet::weight(::WeightInfo::set_auto_compound( + *candidate_auto_compounding_delegation_count_hint, + *delegation_count_hint, + ))] + pub fn set_auto_compound( + origin: OriginFor, + candidate: T::AccountId, + value: Percent, + candidate_auto_compounding_delegation_count_hint: u32, + delegation_count_hint: u32, + ) -> DispatchResultWithPostInfo { + let delegator = ensure_signed(origin)?; + >::set_auto_compound( + candidate, + delegator, + value, + candidate_auto_compounding_delegation_count_hint, + delegation_count_hint, + ) + } + + /// Hotfix to remove existing empty entries for candidates that have left. + #[pallet::call_index(28)] + #[pallet::weight( + T::DbWeight::get().reads_writes(2 * candidates.len() as u64, candidates.len() as u64) + )] + pub fn hotfix_remove_delegation_requests_exited_candidates( + origin: OriginFor, + candidates: Vec, + ) -> DispatchResult { + ensure_signed(origin)?; + ensure!(candidates.len() < 100, >::InsufficientBalance); + for candidate in &candidates { + ensure!(>::get(candidate).is_none(), >::CandidateNotLeaving); + ensure!(>::get(candidate).is_empty(), >::CandidateNotLeaving); + } + + for candidate in candidates { + >::remove(candidate); + } + + Ok(()) + } + } + + /// Represents a payout made via `pay_one_collator_reward`. + pub(crate) enum RewardPayment { + /// A collator was paid + Paid, + /// A collator was skipped for payment. This can happen if they haven't been awarded any + /// points, that is, they did not produce any blocks. + Skipped, + /// All collator payments have been processed. + Finished, + } + + impl Pallet { + pub fn is_delegator(acc: &T::AccountId) -> bool { + >::get(acc).is_some() + } + + pub fn is_candidate(acc: &T::AccountId) -> bool { + >::get(acc).is_some() + } + + pub fn is_selected_candidate(acc: &T::AccountId) -> bool { + >::get().binary_search(acc).is_ok() + } + + /// Returns an account's free balance which is not locked in delegation staking + pub fn get_delegator_stakable_free_balance(acc: &T::AccountId) -> BalanceOf { + T::Currency::reducible_balance(acc, Preservation::Preserve, Fortitude::Force) + // if let Some(state) = >::get(acc) { + // balance = balance.saturating_sub(state.total()); + // } + } + + /// Returns an account's free balance which is not locked in collator staking + pub fn get_collator_stakable_free_balance(acc: &T::AccountId) -> BalanceOf { + T::Currency::reducible_balance(acc, Preservation::Preserve, Fortitude::Force) + // if let Some(info) = >::get(acc) { + // balance = balance.saturating_sub(info.bond); + // } + } + + /// Returns a delegations auto-compound value. + pub fn delegation_auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) -> Percent { + >::auto_compound(candidate, delegator) + } + + /// Caller must ensure candidate is active before calling + pub(crate) fn update_active(candidate: T::AccountId, total: BalanceOf) { + let mut candidates = >::get(); + candidates.remove(&Bond::from_owner(candidate.clone())); + candidates.insert(Bond { owner: candidate, amount: total }); + >::put(candidates); + } + + /// Compute round issuance based on total staked for the given round + fn compute_issuance(staked: BalanceOf) -> BalanceOf { + let config = >::get(); + let round_issuance = crate::inflation::round_issuance_range::(config.round); + // TODO: consider interpolation instead of bounded range + if staked < config.expect.min { + round_issuance.min + } else if staked > config.expect.max { + round_issuance.max + } else { + round_issuance.ideal + } + } + + /// Remove delegation from candidate state + /// Amount input should be retrieved from delegator and it informs the storage lookups + pub(crate) fn delegator_leaves_candidate( + candidate: T::AccountId, + delegator: T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + let mut state = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + state.rm_delegation_if_exists::(&candidate, delegator.clone(), amount)?; + let new_total_locked = >::get().saturating_sub(amount); + >::put(new_total_locked); + let new_total = state.total_counted; + >::insert(&candidate, state); + Self::deposit_event(Event::DelegatorLeftCandidate { + delegator, + candidate, + unstaked_amount: amount, + total_candidate_staked: new_total, + }); + Ok(()) + } + + pub(crate) fn prepare_staking_payouts(now: RoundIndex) -> Weight { + // payout is now - delay rounds ago => now - delay > 0 else return early + let delay = T::RewardPaymentDelay::get(); + if now <= delay { + return Weight::zero(); + } + let round_to_payout = now.saturating_sub(delay); + let total_points = >::get(round_to_payout); + if total_points.is_zero() { + return Weight::zero(); + } + let total_staked = >::take(round_to_payout); + let total_issuance = Self::compute_issuance(total_staked); + + // reserve portion of issuance for parachain bond account. In our situation the + // percentage will be 0% as we don't want to reserve any issuance for parachain + // bond so the logic is commented out + + // let mut left_issuance = total_issuance; + // let bond_config = >::get(); + // let parachain_bond_reserve = bond_config.percent * total_issuance; + // if let Ok(amount_transferred) = + // T::Currency::transfer(&T::PayMaster::get(), &bond_config.account, parachain_bond_reserve, Preservation::Preserve) + // { + // // update round issuance iff transfer succeeds + // left_issuance = left_issuance.saturating_sub(amount_transferred); + // Self::deposit_event(Event::ReservedForParachainBond { + // account: bond_config.account, + // value: amount_transferred, + // }); + // } + + let payout = DelayedPayout { + round_issuance: total_issuance, + total_staking_reward: total_issuance, + collator_commission: >::get(), + }; + + >::insert(round_to_payout, payout); + T::WeightInfo::prepare_staking_payouts() + } + + /// Wrapper around pay_one_collator_reward which handles the following logic: + /// * whether or not a payout needs to be made + /// * cleaning up when payouts are done + /// * returns the weight consumed by pay_one_collator_reward if applicable + fn handle_delayed_payouts(now: RoundIndex) -> Weight { + let delay = T::RewardPaymentDelay::get(); + + // don't underflow uint + if now < delay { + return Weight::from_parts(0u64, 0); + } + + let paid_for_round = now.saturating_sub(delay); + + if let Some(payout_info) = >::get(paid_for_round) { + let result = Self::pay_one_collator_reward(paid_for_round, payout_info); + + // clean up storage items that we no longer need + if matches!(result.0, RewardPayment::Finished) { + >::remove(paid_for_round); + >::remove(paid_for_round); + } + result.1 // weight consumed by pay_one_collator_reward + } else { + Weight::from_parts(0u64, 0) + } + } + + /// Payout a single collator from the given round. + /// + /// Returns an optional tuple of (Collator's AccountId, total paid) + /// or None if there were no more payouts to be made for the round. + pub(crate) fn pay_one_collator_reward( + paid_for_round: RoundIndex, + payout_info: DelayedPayout>, + ) -> (RewardPayment, Weight) { + // 'early_weight' tracks weight used for reads/writes done early in this fn before its + // early-exit codepaths. + let mut early_weight = Weight::zero(); + + // TODO: it would probably be optimal to roll Points into the DelayedPayouts storage + // item so that we do fewer reads each block + let total_points = >::get(paid_for_round); + early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 0)); + + if total_points.is_zero() { + // TODO: this case is obnoxious... it's a value query, so it could mean one of two + // different logic errors: + // 1. we removed it before we should have + // 2. we called pay_one_collator_reward when we were actually done with deferred + // payouts + log::warn!("pay_one_collator_reward called with no > for the round!"); + return (RewardPayment::Finished, early_weight); + } + + let collator_fee = payout_info.collator_commission; + let collator_issuance = collator_fee * payout_info.round_issuance; + + if let Some((collator, state)) = >::iter_prefix(paid_for_round).drain().next() { + // read and kill AtStake + early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + + // Take the awarded points for the collator + let pts = >::take(paid_for_round, &collator); + // read and kill AwardedPts + early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + if pts == 0 { + return (RewardPayment::Skipped, early_weight); + } + + // 'extra_weight' tracks weight returned from fns that we delegate to which can't be + // known ahead of time. + let mut extra_weight = Weight::zero(); + let pct_due = Perbill::from_rational(pts, total_points); + let total_paid = pct_due * payout_info.total_staking_reward; + let mut amt_due = total_paid; + + let num_delegators = state.delegations.len(); + if state.delegations.is_empty() { + // solo collator with no delegators + extra_weight = extra_weight + .saturating_add(T::PayoutCollatorReward::payout_collator_reward( + paid_for_round, + collator.clone(), + amt_due, + )) + .saturating_add(T::OnCollatorPayout::on_collator_payout(paid_for_round, collator, amt_due)); + log::warn!("💵 Solo collator reward: {:?}", amt_due); + } else { + // pay collator first; commission + due_portion + let collator_pct = Perbill::from_rational(state.bond, state.total); + let commission = pct_due * collator_issuance; + amt_due = amt_due.saturating_sub(commission); + let collator_reward = (collator_pct * amt_due).saturating_add(commission); + extra_weight = extra_weight + .saturating_add(T::PayoutCollatorReward::payout_collator_reward( + paid_for_round, + collator.clone(), + collator_reward, + )) + .saturating_add(T::OnCollatorPayout::on_collator_payout( + paid_for_round, + collator.clone(), + collator_reward, + )); + + // pay delegators due portion + for BondWithAutoCompound { owner, amount, auto_compound } in state.delegations { + let percent = Perbill::from_rational(amount, state.total); + let due = percent * amt_due; + if !due.is_zero() { + extra_weight = extra_weight.saturating_add(Self::mint_and_compound( + due, + auto_compound, + collator.clone(), + owner.clone(), + )); + } + } + } + + ( + RewardPayment::Paid, + T::WeightInfo::pay_one_collator_reward(num_delegators as u32).saturating_add(extra_weight), + ) + } else { + // Note that we don't clean up storage here; it is cleaned up in + // handle_delayed_payouts() + (RewardPayment::Finished, Weight::from_parts(0u64, 0)) + } + } + + /// Compute the top `TotalSelected` candidates in the CandidatePool and return + /// a vec of their AccountIds (sorted by AccountId). + /// + /// If the returned vec is empty, the previous candidates should be used. + pub fn compute_top_candidates() -> Vec { + let top_n = >::get() as usize; + if top_n == 0 { + return vec![]; + } + + let mut candidates = >::get().0; + + // If the number of candidates is greater than top_n, select the candidates with higher + // amount. Otherwise, return all the candidates. + if candidates.len() > top_n { + // Partially sort candidates such that element at index `top_n - 1` is sorted, and + // all the elements in the range 0..top_n are the top n elements. + candidates.select_nth_unstable_by(top_n - 1, |a, b| { + // Order by amount, then owner. The owner is needed to ensure a stable order + // when two accounts have the same amount. + a.amount.cmp(&b.amount).then_with(|| a.owner.cmp(&b.owner)).reverse() + }); + + let mut collators = candidates.into_iter().take(top_n).map(|x| x.owner).collect::>(); + + // Sort collators by AccountId + collators.sort(); + + collators + } else { + // Return all candidates + // The candidates are already sorted by AccountId, so no need to sort again + candidates.into_iter().map(|x| x.owner).collect::>() + } + } + + /// Best as in most cumulatively supported in terms of stake + /// Returns [collator_count, delegation_count, total staked] + pub(crate) fn select_top_candidates(now: RoundIndex) -> (Weight, u32, u32, BalanceOf) { + let (mut collator_count, mut delegation_count, mut total) = (0u32, 0u32, BalanceOf::::zero()); + // choose the top TotalSelected qualified candidates, ordered by stake + let collators = Self::compute_top_candidates(); + if collators.is_empty() { + // SELECTION FAILED TO SELECT >=1 COLLATOR => select collators from previous round + let last_round = now.saturating_sub(1u32); + let mut total_per_candidate: BTreeMap> = BTreeMap::new(); + // set this round AtStake to last round AtStake + for (account, snapshot) in >::iter_prefix(last_round) { + collator_count = collator_count.saturating_add(1u32); + delegation_count = delegation_count.saturating_add(snapshot.delegations.len() as u32); + total = total.saturating_add(snapshot.total); + total_per_candidate.insert(account.clone(), snapshot.total); + >::insert(now, account, snapshot); + } + // `SelectedCandidates` remains unchanged from last round + // emit CollatorChosen event for tools that use this event + for candidate in >::get() { + let snapshot_total = + total_per_candidate.get(&candidate).expect("all selected candidates have snapshots"); + Self::deposit_event(Event::CollatorChosen { + round: now, + collator_account: candidate, + total_exposed_amount: *snapshot_total, + }) + } + let weight = T::WeightInfo::select_top_candidates(0, 0); + return (weight, collator_count, delegation_count, total); + } + + // snapshot exposure for round for weighting reward distribution + for account in collators.iter() { + let state = >::get(account).expect("all members of CandidateQ must be candidates"); + + collator_count = collator_count.saturating_add(1u32); + delegation_count = delegation_count.saturating_add(state.delegation_count); + total = total.saturating_add(state.total_counted); + let CountedDelegations { uncounted_stake, rewardable_delegations } = + Self::get_rewardable_delegators(account); + let total_counted = state.total_counted.saturating_sub(uncounted_stake); + + let auto_compounding_delegations = >::get(account) + .into_iter() + .map(|x| (x.delegator, x.value)) + .collect::>(); + let rewardable_delegations = rewardable_delegations + .into_iter() + .map(|d| BondWithAutoCompound { + owner: d.owner.clone(), + amount: d.amount, + auto_compound: auto_compounding_delegations + .get(&d.owner) + .cloned() + .unwrap_or_else(Percent::zero), + }) + .collect(); + + let snapshot = + CollatorSnapshot { bond: state.bond, delegations: rewardable_delegations, total: total_counted }; + >::insert(now, account, snapshot); + Self::deposit_event(Event::CollatorChosen { + round: now, + collator_account: account.clone(), + total_exposed_amount: state.total_counted, + }); + } + // insert canonical collator set + >::put(collators); + + let avg_delegator_count = delegation_count.checked_div(collator_count).unwrap_or(0); + let weight = T::WeightInfo::select_top_candidates(collator_count, avg_delegator_count); + (weight, collator_count, delegation_count, total) + } + + /// Apply the delegator intent for revoke and decrease in order to build the + /// effective list of delegators with their intended bond amount. + /// + /// This will: + /// - if [DelegationChange::Revoke] is outstanding, set the bond amount to 0. + /// - if [DelegationChange::Decrease] is outstanding, subtract the bond by specified amount. + /// - else, do nothing + /// + /// The intended bond amounts will be used while calculating rewards. + pub(crate) fn get_rewardable_delegators(collator: &T::AccountId) -> CountedDelegations { + let requests = >::get(collator) + .into_iter() + .map(|x| (x.delegator, x.action)) + .collect::>(); + let mut uncounted_stake = BalanceOf::::zero(); + let rewardable_delegations = >::get(collator) + .expect("all members of CandidateQ must be candidates") + .delegations + .into_iter() + .map(|mut bond| { + bond.amount = match requests.get(&bond.owner) { + None => bond.amount, + Some(DelegationAction::Revoke(_)) => { + uncounted_stake = uncounted_stake.saturating_add(bond.amount); + BalanceOf::::zero() + }, + Some(DelegationAction::Decrease(amount)) => { + uncounted_stake = uncounted_stake.saturating_add(*amount); + bond.amount.saturating_sub(*amount) + }, + }; + + bond + }) + .collect(); + CountedDelegations { uncounted_stake, rewardable_delegations } + } + + /// This function exists as a helper to delegator_bond_more & auto_compound functionality. + /// Any changes to this function must align with both user-initiated bond increases and + /// auto-compounding bond increases. + /// Any feature-specific preconditions should be validated before this function is invoked. + /// Any feature-specific events must be emitted after this function is invoked. + pub fn delegation_bond_more_without_event( + delegator: T::AccountId, + candidate: T::AccountId, + more: BalanceOf, + ) -> Result { + ensure!( + !Self::delegation_request_revoke_exists(&candidate, &delegator), + Error::::PendingDelegationRevoke + ); + let mut state = >::get(&delegator).ok_or(Error::::DelegatorDNE)?; + state.increase_delegation::(candidate, more) + } + + /// Mint a specified reward amount to the beneficiary account. Emits the [Rewarded] event. + pub fn mint(amt: BalanceOf, to: T::AccountId) { + if let Ok(amount_transferred) = + T::Currency::transfer(&T::PayMaster::get(), &to, amt, Preservation::Preserve) + { + Self::deposit_event(Event::Rewarded { account: to.clone(), rewards: amount_transferred }); + } + } + + /// Mint a specified reward amount to the collator's account. Emits the [Rewarded] event. + pub fn mint_collator_reward( + _paid_for_round: RoundIndex, + collator_id: T::AccountId, + amt: BalanceOf, + ) -> Weight { + if let Ok(amount_transferred) = + T::Currency::transfer(&T::PayMaster::get(), &collator_id, amt, Preservation::Preserve) + { + Self::deposit_event(Event::Rewarded { account: collator_id.clone(), rewards: amount_transferred }); + } + T::WeightInfo::mint_collator_reward() + } + + /// Mint and compound delegation rewards. The function mints the amount towards the + /// delegator and tries to compound a specified percent of it back towards the delegation. + /// If a scheduled delegation revoke exists, then the amount is only minted, and nothing is + /// compounded. Emits the [Compounded] event. + pub fn mint_and_compound( + amt: BalanceOf, + compound_percent: Percent, + candidate: T::AccountId, + delegator: T::AccountId, + ) -> Weight { + let mut weight = T::WeightInfo::mint_collator_reward(); + if let Ok(amount_transferred) = + T::Currency::transfer(&T::PayMaster::get(), &delegator, amt, Preservation::Preserve) + { + Self::deposit_event(Event::Rewarded { account: delegator.clone(), rewards: amount_transferred }); + + let compound_amount = compound_percent.mul_ceil(amount_transferred); + if compound_amount.is_zero() { + return weight; + } + + if let Err(err) = + Self::delegation_bond_more_without_event(delegator.clone(), candidate.clone(), compound_amount) + { + log::warn!( + "skipped compounding staking reward towards candidate '{:?}' for delegator '{:?}': {:?}", + candidate, + delegator, + err + ); + return weight; + }; + weight = weight.saturating_add(T::WeightInfo::delegator_bond_more()); + + Pallet::::deposit_event(Event::Compounded { delegator, candidate, amount: compound_amount }); + }; + + weight + } + } + + /// Add reward points to block authors: + /// * 20 points to the block producer for producing a block in the chain + impl Pallet { + fn award_points_to_block_author(author: AccountIdOf) { + let now = >::get().current; + let score_plus_20 = >::get(now, &author).saturating_add(20); + >::insert(now, author, score_plus_20); + >::mutate(now, |x| *x = x.saturating_add(20)); + } + } + + impl Get> for Pallet { + fn get() -> Vec { + Self::selected_candidates() + } + } + impl pallet_authorship::EventHandler, BlockNumberFor> for Pallet + where + T: Config + pallet_authorship::Config + pallet_session::Config, + { + /// Add reward points to block authors: + /// * 20 points to the block producer for producing a block in the chain + fn note_author(author: AccountIdOf) { + Pallet::::award_points_to_block_author(author); + } + } + + impl pallet_session::SessionManager> for Pallet { + /// 1. A new session starts. + /// 2. In hook new_session: Read the current top n candidates from the + /// TopCandidates and assign this set to author blocks for the next + /// session. + /// 3. AuRa queries the authorities from the session pallet for + /// this session and picks authors on round-robin-basis from list of + /// authorities. + fn new_session(new_index: SessionIndex) -> Option>> { + log::warn!( + "assembling new collators for new session {} at #{:?}", + new_index, + >::block_number(), + ); + + let collators = Pallet::::selected_candidates().to_vec(); + if collators.is_empty() { + // we never want to pass an empty set of collators. This would brick the chain. + log::error!("💥 keeping old session because of empty collator set!"); + None + } else { + Some(collators) + } + } + + fn end_session(_end_index: SessionIndex) { + // we too are not caring. + } + + fn start_session(_start_index: SessionIndex) { + // we too are not caring. + } + } + + impl pallet_session::ShouldEndSession> for Pallet { + fn should_end_session(now: BlockNumberFor) -> bool { + let round = >::get(); + // always update when a new round should start + round.should_update(now) + } + } + + impl EstimateNextSessionRotation> for Pallet { + fn average_session_length() -> BlockNumberFor { + BlockNumberFor::::from(>::get().length) + } + + fn estimate_current_session_progress(now: BlockNumberFor) -> (Option, Weight) { + let round = >::get(); + let passed_blocks = now.saturating_sub(round.first); + + ( + Some(Permill::from_rational(passed_blocks, BlockNumberFor::::from(round.length))), + // One read for the round info, blocknumber is read free + T::DbWeight::get().reads(1), + ) + } + + fn estimate_next_session_rotation(_now: BlockNumberFor) -> (Option>, Weight) { + let round = >::get(); + + ( + Some(round.first + round.length.into()), + // One read for the round info, blocknumber is read free + T::DbWeight::get().reads(1), + ) + } + } +} diff --git a/pallets/parachain-staking/src/migrations.rs b/pallets/parachain-staking/src/migrations.rs new file mode 100644 index 0000000..bc0a139 --- /dev/null +++ b/pallets/parachain-staking/src/migrations.rs @@ -0,0 +1,161 @@ +// 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 . + +//! # Migrations +//! +#[allow(unused_imports)] +use crate::*; + +// Substrate +use frame_support::traits::{ + fungible::{InspectHold, MutateHold}, + Currency, Get, LockIdentifier, LockableCurrency, ReservableCurrency, +}; +#[allow(unused_imports)] +use frame_support::{migration, storage::unhashed}; +use log; +use codec::Encode; +use sp_core::hexdisplay::HexDisplay; +#[cfg(feature = "try-runtime")] +use sp_runtime::DispatchError; +#[allow(unused_imports)] +use sp_std::vec::Vec; + +// Lock Identifiers used in the old version of the pallet. +const COLLATOR_LOCK_ID: LockIdentifier = *b"stkngcol"; +const DELEGATOR_LOCK_ID: LockIdentifier = *b"stkngdel"; + +pub struct CustomOnRuntimeUpgrade +where + T: Config, + OldCurrency: 'static + + LockableCurrency<::AccountId> + + Currency<::AccountId> + + ReservableCurrency<::AccountId>, +{ + _phantom: sp_std::marker::PhantomData<(T, OldCurrency)>, +} + +impl frame_support::traits::OnRuntimeUpgrade for CustomOnRuntimeUpgrade +where + T: Config, + OldCurrency: 'static + + LockableCurrency<::AccountId> + + Currency<::AccountId> + + ReservableCurrency<::AccountId>, + BalanceOf: From, +{ + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, DispatchError> { + let active_collators = CandidatePool::::get().0; + + for bond_info in active_collators { + let owner = bond_info.owner; + let balance = OldCurrency::free_balance(&owner); + log::info!( + "Collator: {:?} OldCurrency::free_balance pre_upgrade {:?}", + HexDisplay::from(&owner.encode()), + balance + ); + } + + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), DispatchError> { + let active_collators = CandidatePool::::get().0; + + for bond_info in active_collators { + let owner = bond_info.owner; + let balance = OldCurrency::free_balance(&owner); + log::info!( + "Collator: {:?} OldCurrency::free_balance post_upgrade {:?}", + HexDisplay::from(&owner.encode()), + balance + ); + } + + Ok(()) + } + + fn on_runtime_upgrade() -> frame_support::weights::Weight { + log::info!("Parachain Staking: on_runtime_upgrade"); + let mut read_ops = 0u64; + let mut write_ops = 0u64; + + // Get all the active collators + let active_collators = CandidatePool::::get().0; + read_ops += 1; + + for bond_info in active_collators { + let owner = bond_info.owner; + log::info!("Parachain Staking: migrating collator {:?}", HexDisplay::from(&owner.encode())); + + let candidate_info = CandidateInfo::::get(&owner).unwrap(); + let bond_amount = candidate_info.bond; + read_ops += 1; + log::info!("Parachain Staking: bond_amount {:?}", bond_amount); + + let already_held: ::Balance = + T::Currency::balance_on_hold(&HoldReason::StakingCollator.into(), &owner); + read_ops += 1; + + // Check if the lock is already held, to make migration idempotent + if already_held == bond_amount { + log::info!("Parachain Staking: already held {:?}", already_held); + } else { + // Remove the lock from the old currency + OldCurrency::remove_lock(COLLATOR_LOCK_ID, &owner); + write_ops += 1; + + // Hold the new currency + T::Currency::hold(&HoldReason::StakingCollator.into(), &owner, bond_amount).unwrap_or_else(|err| { + log::error!("Failed to add lock to parachain staking currency: {:?}", err); + }); + write_ops += 1; + + // Get all the delegations for the collator + if let Some(delegations) = TopDelegations::::get(&owner) { + read_ops += 1; + for delegation in delegations.delegations { + // Process each delegation + log::info!( + "Delegator: {:?}, Amount: {:?}", + HexDisplay::from(&delegation.owner.encode()), + delegation.amount + ); + + // Remove the lock from the old currency + OldCurrency::remove_lock(DELEGATOR_LOCK_ID, &delegation.owner); + write_ops += 1; + + // Hold the new currency + T::Currency::hold(&HoldReason::StakingDelegator.into(), &delegation.owner, delegation.amount) + .unwrap_or_else(|err| { + log::error!("Failed to add lock to parachain staking currency: {:?}", err); + }); + write_ops += 1; + } + } else { + // Handle the case where there are no delegations for the account + log::info!("No delegations found for the given account."); + } + } + } + + log::info!("Parachain Staking: read_ops {:?} | write_ops: {:?}", read_ops, write_ops); + + ::DbWeight::get().reads_writes(read_ops, write_ops) + } +} diff --git a/pallets/parachain-staking/src/mock.rs b/pallets/parachain-staking/src/mock.rs new file mode 100644 index 0000000..52943fc --- /dev/null +++ b/pallets/parachain-staking/src/mock.rs @@ -0,0 +1,928 @@ +// 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 . + +//! Test utilities +use crate as pallet_parachain_staking; +use crate::{pallet, AwardedPts, Config, Event as ParachainStakingEvent, InflationInfo, Points, Range}; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{OnFinalize, OnInitialize}, + weights::{constants::RocksDbWeight, Weight}, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_io; +use sp_runtime::{traits::IdentityLookup, BuildStorage, Perbill, Percent}; + +pub type AccountId = u64; +pub type Balance = u128; +pub type BlockNumber = BlockNumberFor; + +type Block = frame_system::mocking::MockBlockU32; + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Aura: pallet_aura::{Pallet, Storage}, + Session: pallet_session::{Pallet, Call, Storage, Event, Config}, + ParachainStaking: pallet_parachain_staking::{Pallet, Call, Storage, Config, Event, HoldReason}, + Authorship: pallet_authorship::{Pallet, Storage}, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const MaximumBlockWeight: Weight = Weight::from_parts(1024, 1); + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type Block = Block; + type BlockHashCount = BlockHashCount; + type DbWeight = RocksDbWeight; + type Lookup = IdentityLookup; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; + pub const MaxHolds: u32 = 10; +} +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type FreezeIdentifier = (); + type MaxFreezes = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); +} +parameter_types! { + #[derive(Debug, Eq, PartialEq)] + pub const MaxCollatorCandidates: u32 = 10; +} +use sp_consensus_aura::sr25519::AuthorityId; +impl pallet_aura::Config for Test { + type AllowMultipleBlocksPerSlot = frame_support::traits::ConstBool; + type AuthorityId = AuthorityId; + type DisabledValidators = (); + type MaxAuthorities = MaxCollatorCandidates; + type SlotDuration = sp_core::ConstU64<12_000>; +} + +impl pallet_authorship::Config for Test { + type EventHandler = ParachainStaking; + type FindAuthor = pallet_session::FindAccountFromAuthorIndex; +} +parameter_types! { + pub const MinimumPeriod: u64 = 1; +} + +impl pallet_timestamp::Config for Test { + type MinimumPeriod = MinimumPeriod; + type Moment = u64; + type OnTimestampSet = Aura; + type WeightInfo = (); +} + +const GENESIS_BLOCKS_PER_ROUND: BlockNumber = 5; +const GENESIS_COLLATOR_COMMISSION: Perbill = Perbill::from_percent(20); +const GENESIS_PARACHAIN_BOND_RESERVE_PERCENT: Percent = Percent::from_percent(0); +const GENESIS_NUM_SELECTED_CANDIDATES: u32 = 5; +parameter_types! { + pub const MinBlocksPerRound: u32 = 3; + pub const LeaveCandidatesDelay: u32 = 2; + pub const CandidateBondLessDelay: u32 = 2; + pub const LeaveDelegatorsDelay: u32 = 2; + pub const RevokeDelegationDelay: u32 = 2; + pub const DelegationBondLessDelay: u32 = 2; + pub const RewardPaymentDelay: u32 = 2; + pub const MinSelectedCandidates: u32 = GENESIS_NUM_SELECTED_CANDIDATES; + pub const MaxTopDelegationsPerCandidate: u32 = 4; + pub const MaxBottomDelegationsPerCandidate: u32 = 4; + pub const MaxDelegationsPerDelegator: u32 = 4; + pub const MinCandidateStk: u128 = 10; + pub const MinDelegatorStk: u128 = 5; + pub const MinDelegation: u128 = 3; + pub const BlockchainOperationTreasury: AccountId = 1337; +} +impl_opaque_keys! { + pub struct MockSessionKeys { + pub aura: Aura, + } +} + +parameter_types! { + pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(17); +} +use sp_runtime::{ + impl_opaque_keys, + traits::{ConvertInto, OpaqueKeys}, +}; + +impl pallet_session::Config for Test { + type Keys = MockSessionKeys; + type NextSessionRotation = ParachainStaking; + type RuntimeEvent = RuntimeEvent; + type SessionHandler = ::KeyTypeIdProviders; + type SessionManager = ParachainStaking; + type ShouldEndSession = ParachainStaking; + type ValidatorId = AccountId; + type ValidatorIdOf = ConvertInto; + type WeightInfo = (); +} +impl Config for Test { + type Balance = Balance; + type CandidateBondLessDelay = CandidateBondLessDelay; + type Currency = Balances; + type DelegationBondLessDelay = DelegationBondLessDelay; + type LeaveCandidatesDelay = LeaveCandidatesDelay; + type LeaveDelegatorsDelay = LeaveDelegatorsDelay; + type MaxBottomDelegationsPerCandidate = MaxBottomDelegationsPerCandidate; + type MaxDelegationsPerDelegator = MaxDelegationsPerDelegator; + type MaxTopDelegationsPerCandidate = MaxTopDelegationsPerCandidate; + type MinBlocksPerRound = MinBlocksPerRound; + type MinCandidateStk = MinCandidateStk; + type MinDelegation = MinDelegation; + type MinDelegatorStk = MinDelegatorStk; + type MinSelectedCandidates = MinSelectedCandidates; + type MonetaryGovernanceOrigin = frame_system::EnsureRoot; + type OnCollatorPayout = (); + type OnNewRound = (); + type PayMaster = BlockchainOperationTreasury; + type PayoutCollatorReward = (); + type RevokeDelegationDelay = RevokeDelegationDelay; + type RewardPaymentDelay = RewardPaymentDelay; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, + // [collator, amount] + collators: Vec<(AccountId, Balance)>, + // [delegator, collator, delegation_amount, auto_compound_percent] + delegations: Vec<(AccountId, AccountId, Balance, Percent)>, + // inflation config + inflation: InflationInfo, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { + balances: vec![], + delegations: vec![], + collators: vec![], + inflation: InflationInfo { + expect: Range { min: 700, ideal: 700, max: 700 }, + // not used + annual: Range { + min: Perbill::from_percent(50), + ideal: Perbill::from_percent(50), + max: Perbill::from_percent(50), + }, + // unrealistically high parameterization, only for testing + round: Range { + min: Perbill::from_percent(5), + ideal: Perbill::from_percent(5), + max: Perbill::from_percent(5), + }, + }, + } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, mut balances: Vec<(AccountId, Balance)>) -> Self { + if !balances.iter().any(|(acc, _)| *acc == BlockchainOperationTreasury::get()) { + balances.push((BlockchainOperationTreasury::get(), 300)); + } + self.balances = balances; + self + } + + pub(crate) fn with_candidates(mut self, collators: Vec<(AccountId, Balance)>) -> Self { + self.collators = collators; + self + } + + pub(crate) fn with_delegations(mut self, delegations: Vec<(AccountId, AccountId, Balance)>) -> Self { + self.delegations = delegations.into_iter().map(|d| (d.0, d.1, d.2, Percent::zero())).collect(); + self + } + + pub(crate) fn with_auto_compounding_delegations( + mut self, + delegations: Vec<(AccountId, AccountId, Balance, Percent)>, + ) -> Self { + self.delegations = delegations; + self + } + + #[allow(dead_code)] + pub(crate) fn with_inflation(mut self, inflation: InflationInfo) -> Self { + self.inflation = inflation; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + let balances_with_ed = self + .balances + .into_iter() + .map(|(account, balance)| (account, balance + ::ExistentialDeposit::get())) + .collect::>(); + pallet_balances::GenesisConfig:: { balances: balances_with_ed } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + pallet_parachain_staking::GenesisConfig:: { + candidates: self.collators, + delegations: self.delegations, + inflation_config: self.inflation, + collator_commission: GENESIS_COLLATOR_COMMISSION, + parachain_bond_reserve_percent: GENESIS_PARACHAIN_BOND_RESERVE_PERCENT, + blocks_per_round: GENESIS_BLOCKS_PER_ROUND, + num_selected_candidates: GENESIS_NUM_SELECTED_CANDIDATES, + } + .assimilate_storage(&mut t) + .expect("Parachain Staking's storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +/// Rolls forward one block. Returns the new block number. +fn roll_one_block() -> BlockNumber { + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::reset_events(); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + ParachainStaking::on_initialize(System::block_number()); + System::block_number() +} + +/// Rolls to the desired block. Returns the number of blocks played. +pub(crate) fn roll_to(n: BlockNumber) -> BlockNumber { + let mut num_blocks = 0; + let mut block = System::block_number(); + while block < n { + block = roll_one_block(); + num_blocks += 1; + } + num_blocks +} + +/// Rolls desired number of blocks. Returns the final block. +pub(crate) fn roll_blocks(num_blocks: u32) -> BlockNumber { + let mut block = System::block_number(); + for _ in 0..num_blocks { + block = roll_one_block(); + } + block +} + +/// Rolls block-by-block to the beginning of the specified round. +/// This will complete the block in which the round change occurs. +/// Returns the number of blocks played. +pub(crate) fn roll_to_round_begin(round: BlockNumber) -> BlockNumber { + let block = (round - 1) * GENESIS_BLOCKS_PER_ROUND; + roll_to(block) +} + +/// Rolls block-by-block to the end of the specified round. +/// The block following will be the one in which the specified round change occurs. +pub(crate) fn roll_to_round_end(round: BlockNumber) -> BlockNumber { + let block = round * GENESIS_BLOCKS_PER_ROUND - 1; + roll_to(block) +} + +pub(crate) fn events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::ParachainStaking(inner) = e { Some(inner) } else { None }) + .collect::>() +} + +/// Asserts that some events were never emitted. +/// +/// # Example +/// +/// ``` +/// assert_no_events!(); +/// ``` +#[macro_export] +macro_rules! assert_no_events { + () => { + similar_asserts::assert_eq!(Vec::>::new(), crate::mock::events()) + }; +} + +/// Asserts that emitted events match exactly the given input. +/// +/// # Example +/// +/// ``` +/// assert_events_eq!( +/// Foo { x: 1, y: 2 }, +/// Bar { value: "test" }, +/// Baz { a: 10, b: 20 }, +/// ); +/// ``` +#[macro_export] +macro_rules! assert_events_eq { + ($event:expr) => { + similar_asserts::assert_eq!(vec![$event], crate::mock::events()); + }; + ($($events:expr,)+) => { + similar_asserts::assert_eq!(vec![$($events,)+], crate::mock::events()); + }; +} + +/// Asserts that some emitted events match the given input. +/// +/// # Example +/// +/// ``` +/// assert_events_emitted!( +/// Foo { x: 1, y: 2 }, +/// Baz { a: 10, b: 20 }, +/// ); +/// ``` +#[macro_export] +macro_rules! assert_events_emitted { + ($event:expr) => { + [$event].into_iter().for_each(|e| assert!( + crate::mock::events().into_iter().find(|x| x == &e).is_some(), + "Event {:?} was not found in events: \n{:#?}", + e, + crate::mock::events() + )); + }; + ($($events:expr,)+) => { + [$($events,)+].into_iter().for_each(|e| assert!( + crate::mock::events().into_iter().find(|x| x == &e).is_some(), + "Event {:?} was not found in events: \n{:#?}", + e, + crate::mock::events() + )); + }; +} + +/// Asserts that some events were never emitted. +/// +/// # Example +/// +/// ``` +/// assert_events_not_emitted!( +/// Foo { x: 1, y: 2 }, +/// Bar { value: "test" }, +/// ); +/// ``` +#[macro_export] +macro_rules! assert_events_not_emitted { + ($event:expr) => { + [$event].into_iter().for_each(|e| assert!( + crate::mock::events().into_iter().find(|x| x != &e).is_some(), + "Event {:?} was unexpectedly found in events: \n{:#?}", + e, + crate::mock::events() + )); + }; + ($($events:expr,)+) => { + [$($events,)+].into_iter().for_each(|e| assert!( + crate::mock::events().into_iter().find(|x| x != &e).is_some(), + "Event {:?} was unexpectedly found in events: \n{:#?}", + e, + crate::mock::events() + )); + }; +} + +/// Asserts that the emitted events are exactly equal to the input patterns. +/// +/// # Example +/// +/// ``` +/// assert_events_eq_match!( +/// Foo { x: 1, .. }, +/// Bar { .. }, +/// Baz { a: 10, b: 20 }, +/// ); +/// ``` +#[macro_export] +macro_rules! assert_events_eq_match { + ($index:expr;) => { + assert_eq!( + $index, + crate::mock::events().len(), + "Found {} extra event(s): \n{:#?}", + crate::mock::events().len()-$index, + crate::mock::events() + ); + }; + ($index:expr; $event:pat_param, $($events:pat_param,)*) => { + assert!( + matches!( + crate::mock::events().get($index), + Some($event), + ), + "Event {:#?} was not found at index {}: \n{:#?}", + stringify!($event), + $index, + crate::mock::events() + ); + assert_events_eq_match!($index+1; $($events,)*); + }; + ($event:pat_param) => { + assert_events_eq_match!(0; $event,); + }; + ($($events:pat_param,)+) => { + assert_events_eq_match!(0; $($events,)+); + }; +} + +/// Asserts that some emitted events match the input patterns. +/// +/// # Example +/// +/// ``` +/// assert_events_emitted_match!( +/// Foo { x: 1, .. }, +/// Baz { a: 10, b: 20 }, +/// ); +/// ``` +#[macro_export] +macro_rules! assert_events_emitted_match { + ($event:pat_param) => { + assert!( + crate::mock::events().into_iter().any(|x| matches!(x, $event)), + "Event {:?} was not found in events: \n{:#?}", + stringify!($event), + crate::mock::events() + ); + }; + ($event:pat_param, $($events:pat_param,)+) => { + assert_events_emitted_match!($event); + $( + assert_events_emitted_match!($events); + )+ + }; +} + +/// Asserts that the input patterns match none of the emitted events. +/// +/// # Example +/// +/// ``` +/// assert_events_not_emitted_match!( +/// Foo { x: 1, .. }, +/// Baz { a: 10, b: 20 }, +/// ); +/// ``` +#[macro_export] +macro_rules! assert_events_not_emitted_match { + ($event:pat_param) => { + assert!( + crate::mock::events().into_iter().any(|x| !matches!(x, $event)), + "Event {:?} was unexpectedly found in events: \n{:#?}", + stringify!($event), + crate::mock::events() + ); + }; + ($event:pat_param, $($events:pat_param,)+) => { + assert_events_not_emitted_match!($event); + $( + assert_events_not_emitted_match!($events); + )+ + }; +} + +// Same storage changes as ParachainStaking::on_finalize +pub(crate) fn set_author(round: BlockNumber, acc: u64, pts: u32) { + >::mutate(round, |p| *p += pts); + >::mutate(round, acc, |p| *p += pts); +} + +#[test] +fn geneses() { + ExtBuilder::default() + .with_balances(vec![(1, 1000), (2, 300), (3, 100), (4, 100), (5, 100), (6, 100), (7, 100), (8, 9), (9, 4)]) + .with_candidates(vec![(1, 500), (2, 200)]) + .with_delegations(vec![(3, 1, 100), (4, 1, 100), (5, 2, 100), (6, 2, 100)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + // collators + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 500); + assert_eq!(Balances::reserved_balance(&1), 500); + assert!(ParachainStaking::is_candidate(&1)); + assert_eq!(Balances::reserved_balance(&2), 200); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&2), 100); + assert!(ParachainStaking::is_candidate(&2)); + // delegators + for x in 3..7 { + assert!(ParachainStaking::is_delegator(&x)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&x), 0); + assert_eq!(Balances::reserved_balance(&x), 100); + } + // uninvolved + for x in 7..10 { + assert!(!ParachainStaking::is_delegator(&x)); + } + // no delegator staking locks + assert_eq!(Balances::reserved_balance(&7), 0); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&7), 100); + assert_eq!(Balances::reserved_balance(&8), 0); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&8), 9); + assert_eq!(Balances::reserved_balance(&9), 0); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&9), 4); + // no collator staking locks + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&7), 100); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&8), 9); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&9), 4); + }); + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + ]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 10)]) + .with_delegations(vec![(6, 1, 10), (7, 1, 10), (8, 2, 10), (9, 2, 10), (10, 1, 10)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + // collators + for x in 1..5 { + assert!(ParachainStaking::is_candidate(&x)); + assert_eq!(Balances::reserved_balance(&x), 20); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&x), 80); + } + assert!(ParachainStaking::is_candidate(&5)); + assert_eq!(Balances::reserved_balance(&5), 10); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&5), 90); + // delegators + for x in 6..11 { + assert!(ParachainStaking::is_delegator(&x)); + assert_eq!(Balances::reserved_balance(&x), 10); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&x), 90); + } + }); +} + +#[test] +fn roll_to_round_begin_works() { + ExtBuilder::default().build().execute_with(|| { + // these tests assume blocks-per-round of 5, as established by GENESIS_BLOCKS_PER_ROUND + assert_eq!(System::block_number(), 1); // we start on block 1 + + let num_blocks = roll_to_round_begin(1); + assert_eq!(System::block_number(), 1); // no-op, we're already on this round + assert_eq!(num_blocks, 0); + + let num_blocks = roll_to_round_begin(2); + assert_eq!(System::block_number(), 5); + assert_eq!(num_blocks, 4); + + let num_blocks = roll_to_round_begin(3); + assert_eq!(System::block_number(), 10); + assert_eq!(num_blocks, 5); + }); +} + +#[test] +fn roll_to_round_end_works() { + ExtBuilder::default().build().execute_with(|| { + // these tests assume blocks-per-round of 5, as established by GENESIS_BLOCKS_PER_ROUND + assert_eq!(System::block_number(), 1); // we start on block 1 + + let num_blocks = roll_to_round_end(1); + assert_eq!(System::block_number(), 4); + assert_eq!(num_blocks, 3); + + let num_blocks = roll_to_round_end(2); + assert_eq!(System::block_number(), 9); + assert_eq!(num_blocks, 5); + + let num_blocks = roll_to_round_end(3); + assert_eq!(System::block_number(), 14); + assert_eq!(num_blocks, 5); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_fails_if_event_missing() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq!( + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 10, + }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_fails_if_event_extra() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq!( + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 10, + }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 200 }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_fails_if_event_wrong_order() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq!( + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 10, + }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_fails_if_event_wrong_value() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq!( + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 10, + }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 50 }, + ); + }); +} + +#[test] +fn test_assert_events_eq_passes_if_all_events_present_single() { + ExtBuilder::default().build().execute_with(|| { + System::deposit_event(ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }); + + assert_events_eq!(ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }); + }); +} + +#[test] +fn test_assert_events_eq_passes_if_all_events_present_multiple() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq!( + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 10, + }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_emitted_fails_if_event_missing() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_emitted!(ParachainStakingEvent::DelegatorExitScheduled { + round: 2, + delegator: 3, + scheduled_exit: 4, + }); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_emitted_fails_if_event_wrong_value() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_emitted!(ParachainStakingEvent::Rewarded { account: 1, rewards: 50 }); + }); +} + +#[test] +fn test_assert_events_emitted_passes_if_all_events_present_single() { + ExtBuilder::default().build().execute_with(|| { + System::deposit_event(ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }); + + assert_events_emitted!(ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }); + }); +} + +#[test] +fn test_assert_events_emitted_passes_if_all_events_present_multiple() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_emitted!( + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_match_fails_if_event_missing() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq_match!(ParachainStakingEvent::CollatorChosen { .. }, ParachainStakingEvent::NewRound { .. },); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_match_fails_if_event_extra() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq_match!( + ParachainStakingEvent::CollatorChosen { .. }, + ParachainStakingEvent::NewRound { .. }, + ParachainStakingEvent::Rewarded { .. }, + ParachainStakingEvent::Rewarded { .. }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_match_fails_if_event_wrong_order() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq_match!( + ParachainStakingEvent::Rewarded { .. }, + ParachainStakingEvent::CollatorChosen { .. }, + ParachainStakingEvent::NewRound { .. }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_eq_match_fails_if_event_wrong_value() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq_match!( + ParachainStakingEvent::CollatorChosen { .. }, + ParachainStakingEvent::NewRound { .. }, + ParachainStakingEvent::Rewarded { rewards: 50, .. }, + ); + }); +} + +#[test] +fn test_assert_events_eq_match_passes_if_all_events_present_single() { + ExtBuilder::default().build().execute_with(|| { + System::deposit_event(ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }); + + assert_events_eq_match!(ParachainStakingEvent::Rewarded { account: 1, .. }); + }); +} + +#[test] +fn test_assert_events_eq_match_passes_if_all_events_present_multiple() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_eq_match!( + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, .. }, + ParachainStakingEvent::NewRound { starting_block: 10, .. }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_emitted_match_fails_if_event_missing() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_emitted_match!(ParachainStakingEvent::DelegatorExitScheduled { round: 2, .. }); + }); +} + +#[test] +#[should_panic] +fn test_assert_events_emitted_match_fails_if_event_wrong_value() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_emitted_match!(ParachainStakingEvent::Rewarded { rewards: 50, .. }); + }); +} + +#[test] +fn test_assert_events_emitted_match_passes_if_all_events_present_single() { + ExtBuilder::default().build().execute_with(|| { + System::deposit_event(ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }); + + assert_events_emitted_match!(ParachainStakingEvent::Rewarded { rewards: 100, .. }); + }); +} + +#[test] +fn test_assert_events_emitted_match_passes_if_all_events_present_multiple() { + ExtBuilder::default().build().execute_with(|| { + inject_test_events(); + + assert_events_emitted_match!( + ParachainStakingEvent::CollatorChosen { total_exposed_amount: 10, .. }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ); + }); +} + +fn inject_test_events() { + [ + ParachainStakingEvent::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 10 }, + ParachainStakingEvent::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 10, + }, + ParachainStakingEvent::Rewarded { account: 1, rewards: 100 }, + ] + .into_iter() + .for_each(System::deposit_event); +} diff --git a/pallets/parachain-staking/src/set.rs b/pallets/parachain-staking/src/set.rs new file mode 100644 index 0000000..7ce8349 --- /dev/null +++ b/pallets/parachain-staking/src/set.rs @@ -0,0 +1,85 @@ +// 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 . + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_runtime::RuntimeDebug; +use sp_std::prelude::*; + +/// An ordered set backed by `Vec` +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Default, Clone, TypeInfo)] +pub struct OrderedSet(pub Vec); + +impl OrderedSet { + /// Create a new empty set + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Create a set from a `Vec`. + /// `v` will be sorted and dedup first. + pub fn from(mut v: Vec) -> Self { + v.sort(); + v.dedup(); + Self::from_sorted_set(v) + } + + /// Create a set from a `Vec`. + /// Assume `v` is sorted and contain unique elements. + pub fn from_sorted_set(v: Vec) -> Self { + Self(v) + } + + /// Insert an element. + /// Return true if insertion happened. + pub fn insert(&mut self, value: T) -> bool { + match self.0.binary_search(&value) { + Ok(_) => false, + Err(loc) => { + self.0.insert(loc, value); + true + }, + } + } + + /// Remove an element. + /// Return true if removal happened. + pub fn remove(&mut self, value: &T) -> bool { + match self.0.binary_search(value) { + Ok(loc) => { + self.0.remove(loc); + true + }, + Err(_) => false, + } + } + + /// Return if the set contains `value` + pub fn contains(&self, value: &T) -> bool { + self.0.binary_search(value).is_ok() + } + + /// Clear the set + pub fn clear(&mut self) { + self.0.clear(); + } +} + +impl From> for OrderedSet { + fn from(v: Vec) -> Self { + Self::from(v) + } +} diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs new file mode 100644 index 0000000..af5d42d --- /dev/null +++ b/pallets/parachain-staking/src/tests.rs @@ -0,0 +1,5448 @@ +// 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 . + +//! # Staking Pallet Unit Tests +//! The unit tests are organized by the call they test. The order matches the order +//! of the calls in the `lib.rs`. +//! 1. Root +//! 2. Monetary Governance +//! 3. Public (Collator, Nominator) +//! 4. Miscellaneous Property-Based Tests + +use crate::{ + assert_events_emitted, assert_events_emitted_match, assert_events_eq, assert_no_events, + auto_compound::{AutoCompoundConfig, AutoCompoundDelegations}, + delegation_requests::{CancelledScheduledRequest, DelegationAction, ScheduledRequest}, + mock::{ + roll_blocks, roll_to, roll_to_round_begin, roll_to_round_end, set_author, Balances, BlockNumber, ExtBuilder, + ParachainStaking, RuntimeOrigin, Test, + }, + AtStake, Bond, CollatorStatus, DelegationScheduledRequests, DelegatorAdded, DelegatorState, DelegatorStatus, Error, + Event, HoldReason, Range, +}; +use frame_support::{ + assert_noop, assert_ok, + traits::tokens::{ + fungible::{Inspect, InspectHold}, + Fortitude, Preservation, + }, +}; +use pallet_balances::PositiveImbalance; +use sp_runtime::{traits::Zero, Perbill, Percent}; + +// ~~ ROOT ~~ + +#[test] +fn invalid_root_origin_fails() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_total_selected(RuntimeOrigin::signed(45), 6u32), + sp_runtime::DispatchError::BadOrigin + ); + assert_noop!( + ParachainStaking::set_collator_commission(RuntimeOrigin::signed(45), Perbill::from_percent(5)), + sp_runtime::DispatchError::BadOrigin + ); + assert_noop!( + ParachainStaking::set_blocks_per_round(RuntimeOrigin::signed(45), 3u32), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +// SET TOTAL SELECTED + +#[test] +fn set_total_selected_event_emits_correctly() { + ExtBuilder::default().build().execute_with(|| { + // before we can bump total_selected we must bump the blocks per round + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 7u32)); + roll_blocks(1); + assert_ok!(ParachainStaking::set_total_selected(RuntimeOrigin::root(), 6u32)); + assert_events_eq!(Event::TotalSelectedSet { old: 5u32, new: 6u32 }); + }); +} + +#[test] +fn set_total_selected_fails_if_above_blocks_per_round() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::round().length, 5); // test relies on this + assert_noop!( + ParachainStaking::set_total_selected(RuntimeOrigin::root(), 6u32), + Error::::RoundLengthMustBeGreaterThanTotalSelectedCollators, + ); + }); +} + +#[test] +fn set_total_selected_fails_if_equal_to_blocks_per_round() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 10u32)); + assert_noop!( + ParachainStaking::set_total_selected(RuntimeOrigin::root(), 10u32), + Error::::RoundLengthMustBeGreaterThanTotalSelectedCollators, + ); + }); +} + +#[test] +fn set_total_selected_passes_if_below_blocks_per_round() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 10u32)); + assert_ok!(ParachainStaking::set_total_selected(RuntimeOrigin::root(), 9u32)); + }); +} + +#[test] +fn set_blocks_per_round_fails_if_below_total_selected() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 20u32)); + assert_ok!(ParachainStaking::set_total_selected(RuntimeOrigin::root(), 15u32)); + assert_noop!( + ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 14u32), + Error::::RoundLengthMustBeGreaterThanTotalSelectedCollators, + ); + }); +} + +#[test] +fn set_blocks_per_round_fails_if_equal_to_total_selected() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 10u32)); + assert_ok!(ParachainStaking::set_total_selected(RuntimeOrigin::root(), 9u32)); + assert_noop!( + ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 9u32), + Error::::RoundLengthMustBeGreaterThanTotalSelectedCollators, + ); + }); +} + +#[test] +fn set_blocks_per_round_passes_if_above_total_selected() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::round().length, 5); // test relies on this + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 6u32)); + }); +} + +#[test] +fn set_total_selected_storage_updates_correctly() { + ExtBuilder::default().build().execute_with(|| { + // round length must be >= total_selected, so update that first + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 10u32)); + + assert_eq!(ParachainStaking::total_selected(), 5u32); + assert_ok!(ParachainStaking::set_total_selected(RuntimeOrigin::root(), 6u32)); + assert_eq!(ParachainStaking::total_selected(), 6u32); + }); +} + +#[test] +fn cannot_set_total_selected_to_current_total_selected() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_total_selected(RuntimeOrigin::root(), 5u32), + Error::::NoWritingSameValue + ); + }); +} + +#[test] +fn cannot_set_total_selected_below_module_min() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_total_selected(RuntimeOrigin::root(), 4u32), + Error::::CannotSetBelowMin + ); + }); +} + +// SET COLLATOR COMMISSION + +#[test] +fn set_collator_commission_event_emits_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_collator_commission(RuntimeOrigin::root(), Perbill::from_percent(5))); + assert_events_eq!(Event::CollatorCommissionSet { + old: Perbill::from_percent(20), + new: Perbill::from_percent(5), + }); + }); +} + +#[test] +fn set_collator_commission_storage_updates_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::collator_commission(), Perbill::from_percent(20)); + assert_ok!(ParachainStaking::set_collator_commission(RuntimeOrigin::root(), Perbill::from_percent(5))); + assert_eq!(ParachainStaking::collator_commission(), Perbill::from_percent(5)); + }); +} + +#[test] +fn cannot_set_collator_commission_to_current_collator_commission() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_collator_commission(RuntimeOrigin::root(), Perbill::from_percent(20)), + Error::::NoWritingSameValue + ); + }); +} + +// SET BLOCKS PER ROUND + +#[test] +fn set_blocks_per_round_event_emits_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 6u32)); + assert_events_eq!(Event::BlocksPerRoundSet { + current_round: 1, + first_block: 0, + old: 5, + new: 6, + new_per_round_inflation_min: Perbill::from_parts(926), + new_per_round_inflation_ideal: Perbill::from_parts(926), + new_per_round_inflation_max: Perbill::from_parts(926), + }); + }); +} + +#[test] +fn set_blocks_per_round_storage_updates_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::round().length, 5); + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 6u32)); + assert_eq!(ParachainStaking::round().length, 6); + }); +} + +#[test] +fn cannot_set_blocks_per_round_below_module_min() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 2u32), + Error::::CannotSetBelowMin + ); + }); +} + +#[test] +fn cannot_set_blocks_per_round_to_current_blocks_per_round() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 5u32), + Error::::NoWritingSameValue + ); + }); +} + +#[test] +fn round_immediately_jumps_if_current_duration_exceeds_new_blocks_per_round() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + // we can't lower the blocks per round because it must be above the number of collators, + // and we can't lower the number of collators because it must be above + // MinSelectedCandidates. so we first raise blocks per round, then lower it. + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 10u32)); + + roll_to(10); + assert_events_emitted!(Event::NewRound { + starting_block: 10, + round: 2, + selected_collators_number: 1, + total_balance: 20 + },); + roll_to(17); + assert_ok!(ParachainStaking::set_blocks_per_round(RuntimeOrigin::root(), 6u32)); + roll_to(18); + assert_events_emitted!(Event::NewRound { + starting_block: 18, + round: 3, + selected_collators_number: 1, + total_balance: 20 + }); + }); +} + +// ~~ MONETARY GOVERNANCE ~~ + +#[test] +fn invalid_monetary_origin_fails() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_staking_expectations( + RuntimeOrigin::signed(45), + Range { min: 3u32.into(), ideal: 4u32.into(), max: 5u32.into() } + ), + sp_runtime::DispatchError::BadOrigin + ); + assert_noop!( + ParachainStaking::set_inflation( + RuntimeOrigin::signed(45), + Range { min: Perbill::from_percent(3), ideal: Perbill::from_percent(4), max: Perbill::from_percent(5) } + ), + sp_runtime::DispatchError::BadOrigin + ); + assert_noop!( + ParachainStaking::set_inflation( + RuntimeOrigin::signed(45), + Range { min: Perbill::from_percent(3), ideal: Perbill::from_percent(4), max: Perbill::from_percent(5) } + ), + sp_runtime::DispatchError::BadOrigin + ); + assert_noop!( + ParachainStaking::set_parachain_bond_account(RuntimeOrigin::signed(45), 11), + sp_runtime::DispatchError::BadOrigin + ); + assert_noop!( + ParachainStaking::set_parachain_bond_reserve_percent(RuntimeOrigin::signed(45), Percent::from_percent(2)), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +// SET STAKING EXPECTATIONS + +#[test] +fn set_staking_event_emits_event_correctly() { + ExtBuilder::default().build().execute_with(|| { + // valid call succeeds + assert_ok!(ParachainStaking::set_staking_expectations( + RuntimeOrigin::root(), + Range { min: 3u128, ideal: 4u128, max: 5u128 } + )); + assert_events_eq!(Event::StakeExpectationsSet { expect_min: 3u128, expect_ideal: 4u128, expect_max: 5u128 }); + }); +} + +#[test] +fn set_staking_updates_storage_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::inflation_config().expect, Range { min: 700, ideal: 700, max: 700 }); + assert_ok!(ParachainStaking::set_staking_expectations( + RuntimeOrigin::root(), + Range { min: 3u128, ideal: 4u128, max: 5u128 } + )); + assert_eq!(ParachainStaking::inflation_config().expect, Range { min: 3u128, ideal: 4u128, max: 5u128 }); + }); +} + +#[test] +fn cannot_set_invalid_staking_expectations() { + ExtBuilder::default().build().execute_with(|| { + // invalid call fails + assert_noop!( + ParachainStaking::set_staking_expectations( + RuntimeOrigin::root(), + Range { min: 5u128, ideal: 4u128, max: 3u128 } + ), + Error::::InvalidSchedule + ); + }); +} + +#[test] +fn cannot_set_same_staking_expectations() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_staking_expectations( + RuntimeOrigin::root(), + Range { min: 3u128, ideal: 4u128, max: 5u128 } + )); + assert_noop!( + ParachainStaking::set_staking_expectations( + RuntimeOrigin::root(), + Range { min: 3u128, ideal: 4u128, max: 5u128 } + ), + Error::::NoWritingSameValue + ); + }); +} + +// SET INFLATION + +#[test] +fn set_inflation_event_emits_correctly() { + ExtBuilder::default().build().execute_with(|| { + let (min, ideal, max): (Perbill, Perbill, Perbill) = + (Perbill::from_percent(3), Perbill::from_percent(4), Perbill::from_percent(5)); + assert_ok!(ParachainStaking::set_inflation(RuntimeOrigin::root(), Range { min, ideal, max })); + assert_events_eq!(Event::InflationSet { + annual_min: min, + annual_ideal: ideal, + annual_max: max, + round_min: Perbill::from_parts(57), + round_ideal: Perbill::from_parts(75), + round_max: Perbill::from_parts(93), + }); + }); +} + +#[test] +fn set_inflation_storage_updates_correctly() { + ExtBuilder::default().build().execute_with(|| { + let (min, ideal, max): (Perbill, Perbill, Perbill) = + (Perbill::from_percent(3), Perbill::from_percent(4), Perbill::from_percent(5)); + assert_eq!( + ParachainStaking::inflation_config().annual, + Range { min: Perbill::from_percent(50), ideal: Perbill::from_percent(50), max: Perbill::from_percent(50) } + ); + assert_eq!( + ParachainStaking::inflation_config().round, + Range { min: Perbill::from_percent(5), ideal: Perbill::from_percent(5), max: Perbill::from_percent(5) } + ); + assert_ok!(ParachainStaking::set_inflation(RuntimeOrigin::root(), Range { min, ideal, max }),); + assert_eq!(ParachainStaking::inflation_config().annual, Range { min, ideal, max }); + assert_eq!( + ParachainStaking::inflation_config().round, + Range { min: Perbill::from_parts(57), ideal: Perbill::from_parts(75), max: Perbill::from_parts(93) } + ); + }); +} + +#[test] +fn cannot_set_invalid_inflation() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_inflation( + RuntimeOrigin::root(), + Range { min: Perbill::from_percent(5), ideal: Perbill::from_percent(4), max: Perbill::from_percent(3) } + ), + Error::::InvalidSchedule + ); + }); +} + +#[test] +fn cannot_set_same_inflation() { + ExtBuilder::default().build().execute_with(|| { + let (min, ideal, max): (Perbill, Perbill, Perbill) = + (Perbill::from_percent(3), Perbill::from_percent(4), Perbill::from_percent(5)); + assert_ok!(ParachainStaking::set_inflation(RuntimeOrigin::root(), Range { min, ideal, max }),); + assert_noop!( + ParachainStaking::set_inflation(RuntimeOrigin::root(), Range { min, ideal, max }), + Error::::NoWritingSameValue + ); + }); +} + +// SET PARACHAIN BOND ACCOUNT + +#[test] +fn set_parachain_bond_account_event_emits_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_parachain_bond_account(RuntimeOrigin::root(), 11)); + assert_events_eq!(Event::ParachainBondAccountSet { old: 0, new: 11 }); + }); +} + +#[test] +fn set_parachain_bond_account_storage_updates_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::parachain_bond_info().account, 0); + assert_ok!(ParachainStaking::set_parachain_bond_account(RuntimeOrigin::root(), 11)); + assert_eq!(ParachainStaking::parachain_bond_info().account, 11); + }); +} + +// SET PARACHAIN BOND RESERVE PERCENT + +#[test] +fn set_parachain_bond_reserve_percent_event_emits_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ParachainStaking::set_parachain_bond_reserve_percent( + RuntimeOrigin::root(), + Percent::from_percent(50) + )); + assert_events_eq!(Event::ParachainBondReservePercentSet { + old: Percent::from_percent(0), + new: Percent::from_percent(50), + }); + }); +} + +#[test] +fn set_parachain_bond_reserve_percent_storage_updates_correctly() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(ParachainStaking::parachain_bond_info().percent, Percent::from_percent(0)); + assert_ok!(ParachainStaking::set_parachain_bond_reserve_percent( + RuntimeOrigin::root(), + Percent::from_percent(50) + )); + assert_eq!(ParachainStaking::parachain_bond_info().percent, Percent::from_percent(50)); + }); +} + +#[test] +fn cannot_set_same_parachain_bond_reserve_percent() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::set_parachain_bond_reserve_percent(RuntimeOrigin::root(), Percent::from_percent(0)), + Error::::NoWritingSameValue + ); + }); +} + +// ~~ PUBLIC ~~ + +// JOIN CANDIDATES + +#[test] +fn join_candidates_event_emits_correctly() { + ExtBuilder::default().with_balances(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 10u128, 0u32)); + assert_events_eq!(Event::JoinedCollatorCandidates { + account: 1, + amount_locked: 10u128, + new_total_amt_locked: 10u128, + }); + }); +} + +#[test] +fn join_candidates_reserves_balance() { + ExtBuilder::default().with_balances(vec![(1, 10)]).build().execute_with(|| { + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 10); + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 10u128, 0u32)); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 0); + }); +} + +#[test] +fn join_candidates_increases_total_staked() { + ExtBuilder::default().with_balances(vec![(1, 10)]).build().execute_with(|| { + assert_eq!(ParachainStaking::total(), 0); + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 10u128, 0u32)); + assert_eq!(ParachainStaking::total(), 10); + }); +} + +#[test] +fn join_candidates_creates_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 10)]).build().execute_with(|| { + assert!(ParachainStaking::candidate_info(1).is_none()); + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 10u128, 0u32)); + let candidate_state = ParachainStaking::candidate_info(1).expect("just joined => exists"); + assert_eq!(candidate_state.bond, 10u128); + }); +} + +#[test] +fn join_candidates_adds_to_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 10)]).build().execute_with(|| { + assert!(ParachainStaking::candidate_pool().0.is_empty()); + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 10u128, 0u32)); + let candidate_pool = ParachainStaking::candidate_pool(); + assert_eq!(candidate_pool.0[0].owner, 1); + assert_eq!(candidate_pool.0[0].amount, 10); + }); +} + +#[test] +fn cannot_join_candidates_if_candidate() { + ExtBuilder::default().with_balances(vec![(1, 1000)]).with_candidates(vec![(1, 500)]).build().execute_with(|| { + assert_noop!( + ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 11u128, 100u32), + Error::::CandidateExists + ); + }); +} + +#[test] +fn cannot_join_candidates_if_delegator() { + ExtBuilder::default() + .with_balances(vec![(1, 50), (2, 20)]) + .with_candidates(vec![(1, 50)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::join_candidates(RuntimeOrigin::signed(2), 10u128, 1u32), + Error::::DelegatorExists + ); + }); +} + +#[test] +fn cannot_join_candidates_without_min_bond() { + ExtBuilder::default().with_balances(vec![(1, 1000)]).build().execute_with(|| { + assert_noop!( + ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 9u128, 100u32), + Error::::CandidateBondBelowMin + ); + }); +} + +#[test] +fn cannot_join_candidates_with_more_than_available_balance() { + ExtBuilder::default().with_balances(vec![(1, 500)]).build().execute_with(|| { + assert_noop!( + ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 501u128, 100u32), + Error::::InsufficientBalance + ); + }); +} + +#[test] +fn insufficient_join_candidates_weight_hint_fails() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + for i in 0..5 { + assert_noop!( + ParachainStaking::join_candidates(RuntimeOrigin::signed(6), 20, i), + Error::::TooLowCandidateCountWeightHintJoinCandidates + ); + } + }); +} + +#[test] +fn sufficient_join_candidates_weight_hint_succeeds() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20), (7, 20), (8, 20), (9, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + let mut count = 5u32; + for i in 6..10 { + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(i), 20, count)); + count += 1u32; + } + }); +} + +// SCHEDULE LEAVE CANDIDATES + +#[test] +fn leave_candidates_event_emits_correctly() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert_events_eq!(Event::CandidateScheduledExit { exit_allowed_round: 1, candidate: 1, scheduled_exit: 3 }); + }); +} + +#[test] +fn leave_candidates_removes_candidate_from_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_eq!(ParachainStaking::candidate_pool().0.len(), 1); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert!(ParachainStaking::candidate_pool().0.is_empty()); + }); +} + +#[test] +fn cannot_leave_candidates_if_not_candidate() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32), + Error::::CandidateDNE + ); + }); +} + +#[test] +fn cannot_leave_candidates_if_already_leaving_candidates() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert_noop!( + ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32), + Error::::CandidateAlreadyLeaving + ); + }); +} + +#[test] +fn insufficient_leave_candidates_weight_hint_fails() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + for i in 1..6 { + assert_noop!( + ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(i), 4u32), + Error::::TooLowCandidateCountToLeaveCandidates + ); + } + }); +} + +#[test] +fn sufficient_leave_candidates_weight_hint_succeeds() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]) + .build() + .execute_with(|| { + let mut count = 5u32; + for i in 1..6 { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(i), count)); + count -= 1u32; + } + }); +} + +// EXECUTE LEAVE CANDIDATES + +#[test] +fn execute_leave_candidates_emits_event() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 0)); + assert_events_emitted!(Event::CandidateLeft { ex_candidate: 1, unlocked_amount: 10, new_total_amt_locked: 0 }); + }); +} + +#[test] +fn execute_leave_candidates_callable_by_any_signed() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(2), 1, 0)); + }); +} + +#[test] +fn execute_leave_candidates_requires_correct_weight_hint() { + ExtBuilder::default() + .with_balances(vec![(1, 10), (2, 10), (3, 10), (4, 10)]) + .with_candidates(vec![(1, 10)]) + .with_delegations(vec![(2, 1, 10), (3, 1, 10), (4, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + roll_to(10); + for i in 0..3 { + assert_noop!( + ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, i), + Error::::TooLowCandidateDelegationCountToLeaveCandidates + ); + } + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(2), 1, 3)); + }); +} + +#[test] +fn execute_leave_candidates_unreserves_balance() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 0); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 0)); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 10); + }); +} + +#[test] +fn execute_leave_candidates_decreases_total_staked() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_eq!(ParachainStaking::total(), 10); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 0)); + assert_eq!(ParachainStaking::total(), 0); + }); +} + +#[test] +fn execute_leave_candidates_removes_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + // candidate state is not immediately removed + let candidate_state = ParachainStaking::candidate_info(1).expect("just left => still exists"); + assert_eq!(candidate_state.bond, 10u128); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 0)); + assert!(ParachainStaking::candidate_info(1).is_none()); + }); +} + +#[test] +fn execute_leave_candidates_removes_pending_delegation_requests() { + ExtBuilder::default() + .with_balances(vec![(1, 10), (2, 15)]) + .with_candidates(vec![(1, 10)]) + .with_delegations(vec![(2, 1, 15)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + let state = ParachainStaking::delegation_scheduled_requests(&1); + assert_eq!( + state, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Decrease(5) }], + ); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + // candidate state is not immediately removed + let candidate_state = ParachainStaking::candidate_info(1).expect("just left => still exists"); + assert_eq!(candidate_state.bond, 10u128); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 1)); + assert!(ParachainStaking::candidate_info(1).is_none()); + assert!( + !ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2), + "delegation request not removed" + ); + assert!(!>::contains_key(&1), "the key was not removed from storage"); + }); +} + +#[test] +fn cannot_execute_leave_candidates_before_delay() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert_noop!( + ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(3), 1, 0), + Error::::CandidateCannotLeaveYet + ); + roll_to(9); + assert_noop!( + ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(3), 1, 0), + Error::::CandidateCannotLeaveYet + ); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(3), 1, 0)); + }); +} + +// CANCEL LEAVE CANDIDATES + +#[test] +fn cancel_leave_candidates_emits_event() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert_ok!(ParachainStaking::cancel_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_events_emitted!(Event::CancelledCandidateExit { candidate: 1 }); + }); +} + +#[test] +fn cancel_leave_candidates_updates_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert_ok!(ParachainStaking::cancel_leave_candidates(RuntimeOrigin::signed(1), 1)); + let candidate = ParachainStaking::candidate_info(&1).expect("just cancelled leave so exists"); + assert!(candidate.is_active()); + }); +} + +#[test] +fn cancel_leave_candidates_adds_to_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 10)]).with_candidates(vec![(1, 10)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1u32)); + assert_ok!(ParachainStaking::cancel_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_eq!(ParachainStaking::candidate_pool().0[0].owner, 1); + assert_eq!(ParachainStaking::candidate_pool().0[0].amount, 10); + }); +} + +// GO OFFLINE + +#[test] +fn go_offline_event_emits_correctly() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + assert_events_eq!(Event::CandidateWentOffline { candidate: 1 }); + }); +} + +#[test] +fn go_offline_removes_candidate_from_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_eq!(ParachainStaking::candidate_pool().0.len(), 1); + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + assert!(ParachainStaking::candidate_pool().0.is_empty()); + }); +} + +#[test] +fn go_offline_updates_candidate_state_to_idle() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + let candidate_state = ParachainStaking::candidate_info(1).expect("is active candidate"); + assert_eq!(candidate_state.status, CollatorStatus::Active); + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + let candidate_state = ParachainStaking::candidate_info(1).expect("is candidate, just offline"); + assert_eq!(candidate_state.status, CollatorStatus::Idle); + }); +} + +#[test] +fn cannot_go_offline_if_not_candidate() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!(ParachainStaking::go_offline(RuntimeOrigin::signed(3)), Error::::CandidateDNE); + }); +} + +#[test] +fn cannot_go_offline_if_already_offline() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + assert_noop!(ParachainStaking::go_offline(RuntimeOrigin::signed(1)), Error::::AlreadyOffline); + }); +} + +// GO ONLINE + +#[test] +fn go_online_event_emits_correctly() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + roll_blocks(1); + assert_ok!(ParachainStaking::go_online(RuntimeOrigin::signed(1))); + assert_events_eq!(Event::CandidateBackOnline { candidate: 1 }); + }); +} + +#[test] +fn go_online_adds_to_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + assert!(ParachainStaking::candidate_pool().0.is_empty()); + assert_ok!(ParachainStaking::go_online(RuntimeOrigin::signed(1))); + assert_eq!(ParachainStaking::candidate_pool().0[0].owner, 1); + assert_eq!(ParachainStaking::candidate_pool().0[0].amount, 20); + }); +} + +#[test] +fn go_online_storage_updates_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::go_offline(RuntimeOrigin::signed(1))); + let candidate_state = ParachainStaking::candidate_info(1).expect("offline still exists"); + assert_eq!(candidate_state.status, CollatorStatus::Idle); + assert_ok!(ParachainStaking::go_online(RuntimeOrigin::signed(1))); + let candidate_state = ParachainStaking::candidate_info(1).expect("online so exists"); + assert_eq!(candidate_state.status, CollatorStatus::Active); + }); +} + +#[test] +fn cannot_go_online_if_not_candidate() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!(ParachainStaking::go_online(RuntimeOrigin::signed(3)), Error::::CandidateDNE); + }); +} + +#[test] +fn cannot_go_online_if_already_online() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_noop!(ParachainStaking::go_online(RuntimeOrigin::signed(1)), Error::::AlreadyActive); + }); +} + +#[test] +fn cannot_go_online_if_leaving() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_noop!(ParachainStaking::go_online(RuntimeOrigin::signed(1)), Error::::CannotGoOnlineIfLeaving); + }); +} + +// CANDIDATE BOND MORE + +#[test] +fn candidate_bond_more_emits_correct_event() { + ExtBuilder::default().with_balances(vec![(1, 50)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::candidate_bond_more(RuntimeOrigin::signed(1), 30)); + assert_events_eq!(Event::CandidateBondedMore { candidate: 1, amount: 30, new_total_bond: 50 }); + }); +} + +#[test] +fn candidate_bond_more_reserves_balance() { + ExtBuilder::default().with_balances(vec![(1, 50)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 30); + assert_ok!(ParachainStaking::candidate_bond_more(RuntimeOrigin::signed(1), 30)); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 0); + }); +} + +#[test] +fn candidate_bond_more_increases_total() { + ExtBuilder::default().with_balances(vec![(1, 50)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + let mut total = ParachainStaking::total(); + assert_ok!(ParachainStaking::candidate_bond_more(RuntimeOrigin::signed(1), 30)); + total += 30; + assert_eq!(ParachainStaking::total(), total); + }); +} + +#[test] +fn candidate_bond_more_updates_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 50)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + let candidate_state = ParachainStaking::candidate_info(1).expect("updated => exists"); + assert_eq!(candidate_state.bond, 20); + assert_ok!(ParachainStaking::candidate_bond_more(RuntimeOrigin::signed(1), 30)); + let candidate_state = ParachainStaking::candidate_info(1).expect("updated => exists"); + assert_eq!(candidate_state.bond, 50); + }); +} + +#[test] +fn candidate_bond_more_updates_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 50)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + assert_eq!(ParachainStaking::candidate_pool().0[0].owner, 1); + assert_eq!(ParachainStaking::candidate_pool().0[0].amount, 20); + assert_ok!(ParachainStaking::candidate_bond_more(RuntimeOrigin::signed(1), 30)); + assert_eq!(ParachainStaking::candidate_pool().0[0].owner, 1); + assert_eq!(ParachainStaking::candidate_pool().0[0].amount, 50); + }); +} + +// SCHEDULE CANDIDATE BOND LESS + +#[test] +fn schedule_candidate_bond_less_event_emits_correctly() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + assert_events_eq!(Event::CandidateBondLessRequested { candidate: 1, amount_to_decrease: 10, execute_round: 3 }); + }); +} + +#[test] +fn cannot_schedule_candidate_bond_less_if_request_exists() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 5)); + assert_noop!( + ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 5), + Error::::PendingCandidateRequestAlreadyExists + ); + }); +} + +#[test] +fn cannot_schedule_candidate_bond_less_if_not_candidate() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(6), 50), + Error::::CandidateDNE + ); + }); +} + +#[test] +fn cannot_schedule_candidate_bond_less_if_new_total_below_min_candidate_stk() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_noop!( + ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 21), + Error::::CandidateBondBelowMin + ); + }); +} + +#[test] +fn can_schedule_candidate_bond_less_if_leaving_candidates() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + }); +} + +#[test] +fn cannot_schedule_candidate_bond_less_if_exited_candidates() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 0)); + assert_noop!( + ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10), + Error::::CandidateDNE + ); + }); +} + +// 2. EXECUTE BOND LESS REQUEST + +#[test] +fn execute_candidate_bond_less_emits_correct_event() { + ExtBuilder::default().with_balances(vec![(1, 50)]).with_candidates(vec![(1, 50)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 30)); + roll_to(10); + roll_blocks(1); + assert_ok!(ParachainStaking::execute_candidate_bond_less(RuntimeOrigin::signed(1), 1)); + assert_events_eq!(Event::CandidateBondedLess { candidate: 1, amount: 30, new_bond: 20 }); + }); +} + +#[test] +fn execute_candidate_bond_less_unreserves_balance() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 0); + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + roll_to(10); + assert_ok!(ParachainStaking::execute_candidate_bond_less(RuntimeOrigin::signed(1), 1)); + assert_eq!(ParachainStaking::get_collator_stakable_free_balance(&1), 10); + }); +} + +#[test] +fn execute_candidate_bond_less_decreases_total() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + let mut total = ParachainStaking::total(); + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + roll_to(10); + assert_ok!(ParachainStaking::execute_candidate_bond_less(RuntimeOrigin::signed(1), 1)); + total -= 10; + assert_eq!(ParachainStaking::total(), total); + }); +} + +#[test] +fn execute_candidate_bond_less_updates_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + let candidate_state = ParachainStaking::candidate_info(1).expect("updated => exists"); + assert_eq!(candidate_state.bond, 30); + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + roll_to(10); + assert_ok!(ParachainStaking::execute_candidate_bond_less(RuntimeOrigin::signed(1), 1)); + let candidate_state = ParachainStaking::candidate_info(1).expect("updated => exists"); + assert_eq!(candidate_state.bond, 20); + }); +} + +#[test] +fn execute_candidate_bond_less_updates_candidate_pool() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_eq!(ParachainStaking::candidate_pool().0[0].owner, 1); + assert_eq!(ParachainStaking::candidate_pool().0[0].amount, 30); + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + roll_to(10); + assert_ok!(ParachainStaking::execute_candidate_bond_less(RuntimeOrigin::signed(1), 1)); + assert_eq!(ParachainStaking::candidate_pool().0[0].owner, 1); + assert_eq!(ParachainStaking::candidate_pool().0[0].amount, 20); + }); +} + +// CANCEL CANDIDATE BOND LESS REQUEST + +#[test] +fn cancel_candidate_bond_less_emits_event() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + assert_ok!(ParachainStaking::cancel_candidate_bond_less(RuntimeOrigin::signed(1))); + assert_events_emitted!(Event::CancelledCandidateBondLess { candidate: 1, amount: 10, execute_round: 3 }); + }); +} + +#[test] +fn cancel_candidate_bond_less_updates_candidate_state() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + assert_ok!(ParachainStaking::cancel_candidate_bond_less(RuntimeOrigin::signed(1))); + assert!(ParachainStaking::candidate_info(&1).unwrap().request.is_none()); + }); +} + +#[test] +fn only_candidate_can_cancel_candidate_bond_less_request() { + ExtBuilder::default().with_balances(vec![(1, 30)]).with_candidates(vec![(1, 30)]).build().execute_with(|| { + assert_ok!(ParachainStaking::schedule_candidate_bond_less(RuntimeOrigin::signed(1), 10)); + assert_noop!( + ParachainStaking::cancel_candidate_bond_less(RuntimeOrigin::signed(2)), + Error::::CandidateDNE + ); + }); +} + +// DELEGATE + +#[test] +fn delegate_event_emits_correctly() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 0, 0)); + assert_events_eq!(Event::Delegation { + delegator: 2, + locked_amount: 10, + candidate: 1, + delegator_position: DelegatorAdded::AddedToTop { new_total: 40 }, + auto_compound: Percent::zero(), + }); + }, + ); +} + +#[test] +fn delegate_reserves_balance() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 10); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 0, 0)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 0); + }, + ); +} + +#[test] +fn delegate_updates_delegator_state() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert!(ParachainStaking::delegator_state(2).is_none()); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 0, 0)); + let delegator_state = ParachainStaking::delegator_state(2).expect("just delegated => exists"); + assert_eq!(delegator_state.total(), 10); + assert_eq!(delegator_state.delegations.0[0].owner, 1); + assert_eq!(delegator_state.delegations.0[0].amount, 10); + }, + ); +} + +#[test] +fn delegate_updates_collator_state() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + let candidate_state = ParachainStaking::candidate_info(1).expect("registered in genesis"); + assert_eq!(candidate_state.total_counted, 30); + let top_delegations = ParachainStaking::top_delegations(1).expect("registered in genesis"); + assert!(top_delegations.delegations.is_empty()); + assert!(top_delegations.total.is_zero()); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 0, 0)); + let candidate_state = ParachainStaking::candidate_info(1).expect("just delegated => exists"); + assert_eq!(candidate_state.total_counted, 40); + let top_delegations = ParachainStaking::top_delegations(1).expect("just delegated => exists"); + assert_eq!(top_delegations.delegations[0].owner, 2); + assert_eq!(top_delegations.delegations[0].amount, 10); + assert_eq!(top_delegations.total, 10); + }, + ); +} + +#[test] +fn can_delegate_immediately_after_other_join_candidates() { + ExtBuilder::default().with_balances(vec![(1, 20), (2, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 20, 0)); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 20, 0, 0)); + }); +} + +#[test] +fn can_delegate_if_revoking() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 30), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 4, 10, 0, 2)); + }); +} + +#[test] +fn cannot_delegate_if_full_and_new_delegation_less_than_or_equal_lowest_bottom() { + ExtBuilder::default() + .with_balances(vec![ + (1, 20), + (2, 10), + (3, 10), + (4, 10), + (5, 10), + (6, 10), + (7, 10), + (8, 10), + (9, 10), + (10, 10), + (11, 10), + ]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![ + (2, 1, 10), + (3, 1, 10), + (4, 1, 10), + (5, 1, 10), + (6, 1, 10), + (8, 1, 10), + (9, 1, 10), + (10, 1, 10), + ]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate(RuntimeOrigin::signed(11), 1, 10, 8, 0), + Error::::CannotDelegateLessThanOrEqualToLowestBottomWhenFull + ); + }); +} + +#[test] +fn can_delegate_if_full_and_new_delegation_greater_than_lowest_bottom() { + ExtBuilder::default() + .with_balances(vec![ + (1, 20), + (2, 10), + (3, 10), + (4, 10), + (5, 10), + (6, 10), + (7, 10), + (8, 10), + (9, 10), + (10, 10), + (11, 11), + ]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![ + (2, 1, 10), + (3, 1, 10), + (4, 1, 10), + (5, 1, 10), + (6, 1, 10), + (8, 1, 10), + (9, 1, 10), + (10, 1, 10), + ]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(11), 1, 11, 8, 0)); + assert_events_emitted!(Event::DelegationKicked { delegator: 10, candidate: 1, unstaked_amount: 10 }); + assert_events_emitted!(Event::DelegatorLeft { delegator: 10, unstaked_amount: 10 }); + }); +} + +#[test] +fn can_still_delegate_if_leaving() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 20), (3, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 3, 10, 0, 1),); + }); +} + +#[test] +fn cannot_delegate_if_candidate() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 30)]) + .with_candidates(vec![(1, 20), (2, 20)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 0, 0), + Error::::CandidateExists + ); + }); +} + +#[test] +fn cannot_delegate_if_already_delegated() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 30)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 20)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 1, 1), + Error::::AlreadyDelegatedCandidate + ); + }); +} + +#[test] +fn cannot_delegate_more_than_max_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 50), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10), (2, 4, 10), (2, 5, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate(RuntimeOrigin::signed(2), 6, 10, 0, 4), + Error::::ExceedMaxDelegationsPerDelegator, + ); + }); +} + +#[test] +fn sufficient_delegate_weight_hint_succeeds() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20), (7, 20), (8, 20), (9, 20), (10, 20)]) + .with_candidates(vec![(1, 20), (2, 20)]) + .with_delegations(vec![(3, 1, 10), (4, 1, 10), (5, 1, 10), (6, 1, 10)]) + .build() + .execute_with(|| { + let mut count = 4u32; + for i in 7..11 { + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(i), 1, 10, count, 0u32)); + count += 1u32; + } + let mut count = 0u32; + for i in 3..11 { + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(i), 2, 10, count, 1u32)); + count += 1u32; + } + }); +} + +#[test] +fn insufficient_delegate_weight_hint_fails() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20), (7, 20), (8, 20), (9, 20), (10, 20)]) + .with_candidates(vec![(1, 20), (2, 20)]) + .with_delegations(vec![(3, 1, 10), (4, 1, 10), (5, 1, 10), (6, 1, 10)]) + .build() + .execute_with(|| { + let mut count = 3u32; + for i in 7..11 { + assert_noop!( + ParachainStaking::delegate(RuntimeOrigin::signed(i), 1, 10, count, 0u32), + Error::::TooLowCandidateDelegationCountToDelegate + ); + } + // to set up for next error test + count = 4u32; + for i in 7..11 { + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(i), 1, 10, count, 0u32)); + count += 1u32; + } + count = 0u32; + for i in 3..11 { + assert_noop!( + ParachainStaking::delegate(RuntimeOrigin::signed(i), 2, 10, count, 0u32), + Error::::TooLowDelegationCountToDelegate + ); + count += 1u32; + } + }); +} + +// SCHEDULE LEAVE DELEGATORS + +#[test] +fn schedule_leave_delegators_event_emits_correctly() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_events_eq!(Event::DelegatorExitScheduled { round: 1, delegator: 2, scheduled_exit: 3 }); + }); +} + +#[test] +fn cannot_schedule_leave_delegators_if_already_leaving() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_noop!( + ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2)), + Error::::DelegatorAlreadyLeaving + ); + }); +} + +#[test] +fn cannot_schedule_leave_delegators_if_not_delegator() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert_noop!( + ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2)), + Error::::DelegatorDNE + ); + }, + ); +} + +// EXECUTE LEAVE DELEGATORS + +#[test] +fn execute_leave_delegators_event_emits_correctly() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + assert_events_emitted!(Event::DelegatorLeft { delegator: 2, unstaked_amount: 10 }); + }); +} + +#[test] +fn execute_leave_delegators_unreserves_balance() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 00); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 10); + assert_eq!(crate::mock::Balances::reserved_balance(&2), 0); + }); +} + +#[test] +fn execute_leave_delegators_decreases_total_staked() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::total(), 40); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::total(), 30); + }); +} + +#[test] +fn execute_leave_delegators_removes_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert!(ParachainStaking::delegator_state(2).is_some()); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + assert!(ParachainStaking::delegator_state(2).is_none()); + }); +} + +#[test] +fn execute_leave_delegators_removes_pending_delegation_requests() { + ExtBuilder::default() + .with_balances(vec![(1, 10), (2, 15)]) + .with_candidates(vec![(1, 10)]) + .with_delegations(vec![(2, 1, 15)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + let state = ParachainStaking::delegation_scheduled_requests(&1); + assert_eq!( + state, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Decrease(5) }], + ); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + assert!(ParachainStaking::delegator_state(2).is_none()); + assert!( + !ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2), + "delegation request not removed" + ) + }); +} + +#[test] +fn execute_leave_delegators_removes_delegations_from_collator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 20), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(2, 20), (3, 20), (4, 20), (5, 20)]) + .with_delegations(vec![(1, 2, 10), (1, 3, 10), (1, 4, 10), (1, 5, 10)]) + .build() + .execute_with(|| { + for i in 2..6 { + let candidate_state = ParachainStaking::candidate_info(i).expect("initialized in ext builder"); + assert_eq!(candidate_state.total_counted, 30); + let top_delegations = ParachainStaking::top_delegations(i).expect("initialized in ext builder"); + assert_eq!(top_delegations.delegations[0].owner, 1); + assert_eq!(top_delegations.delegations[0].amount, 10); + assert_eq!(top_delegations.total, 10); + } + assert_eq!(ParachainStaking::delegator_state(1).unwrap().delegations.0.len(), 4usize); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(1))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(1), 1, 10)); + for i in 2..6 { + let candidate_state = ParachainStaking::candidate_info(i).expect("initialized in ext builder"); + assert_eq!(candidate_state.total_counted, 20); + let top_delegations = ParachainStaking::top_delegations(i).expect("initialized in ext builder"); + assert!(top_delegations.delegations.is_empty()); + } + }); +} + +#[test] +fn cannot_execute_leave_delegators_before_delay() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_noop!( + ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1), + Error::::DelegatorCannotLeaveYet + ); + // can execute after delay + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + }); +} + +#[test] +fn cannot_execute_leave_delegators_if_single_delegation_revoke_manually_cancelled() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 3)); + roll_to(10); + assert_noop!( + ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 2), + Error::::DelegatorNotLeaving + ); + // can execute after manually scheduling revoke, and the round delay after which + // all revokes can be executed + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 3)); + roll_to(20); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 2)); + }); +} + +#[test] +fn insufficient_execute_leave_delegators_weight_hint_fails() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(3, 1, 10), (4, 1, 10), (5, 1, 10), (6, 1, 10)]) + .build() + .execute_with(|| { + for i in 3..7 { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(i))); + } + roll_to(10); + for i in 3..7 { + assert_noop!( + ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(i), i, 0), + Error::::TooLowDelegationCountToLeaveDelegators + ); + } + }); +} + +#[test] +fn sufficient_execute_leave_delegators_weight_hint_succeeds() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(3, 1, 10), (4, 1, 10), (5, 1, 10), (6, 1, 10)]) + .build() + .execute_with(|| { + for i in 3..7 { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(i))); + } + roll_to(10); + for i in 3..7 { + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(i), i, 1)); + } + }); +} + +// CANCEL LEAVE DELEGATORS + +#[test] +fn cancel_leave_delegators_emits_correct_event() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_ok!(ParachainStaking::cancel_leave_delegators(RuntimeOrigin::signed(2))); + assert_events_emitted!(Event::DelegatorExitCancelled { delegator: 2 }); + }); +} + +#[test] +fn cancel_leave_delegators_updates_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_ok!(ParachainStaking::cancel_leave_delegators(RuntimeOrigin::signed(2))); + let delegator = ParachainStaking::delegator_state(&2).expect("just cancelled exit so exists"); + assert!(delegator.is_active()); + }); +} + +#[test] +fn cannot_cancel_leave_delegators_if_single_delegation_revoke_manually_cancelled() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 3)); + roll_to(10); + assert_noop!( + ParachainStaking::cancel_leave_delegators(RuntimeOrigin::signed(2)), + Error::::DelegatorNotLeaving + ); + // can execute after manually scheduling revoke, without waiting for round delay after + // which all revokes can be executed + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 3)); + assert_ok!(ParachainStaking::cancel_leave_delegators(RuntimeOrigin::signed(2))); + }); +} + +// SCHEDULE REVOKE DELEGATION + +#[test] +fn revoke_delegation_event_emits_correctly() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_events_eq!(Event::DelegationRevocationScheduled { + round: 1, + delegator: 2, + candidate: 1, + scheduled_exit: 3, + }); + roll_to_round_begin(3); + roll_blocks(1); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_events_eq!( + Event::DelegatorLeftCandidate { + delegator: 2, + candidate: 1, + unstaked_amount: 10, + total_candidate_staked: 30 + }, + Event::DelegationRevoked { delegator: 2, candidate: 1, unstaked_amount: 10 }, + ); + }); +} + +#[test] +fn can_revoke_delegation_if_revoking_another_delegation() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + // this is an exit implicitly because last delegation revoked + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 3)); + }); +} + +#[test] +fn delegator_not_allowed_revoke_if_already_leaving() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_noop!( + ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 3), + >::PendingDelegationRequestAlreadyExists, + ); + }); +} + +#[test] +fn cannot_revoke_delegation_if_not_delegator() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1), + Error::::DelegatorDNE + ); + }); +} + +#[test] +fn cannot_revoke_delegation_that_dne() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 3), + Error::::DelegationDNE + ); + }); +} + +#[test] +// See `cannot_execute_revoke_delegation_below_min_delegator_stake` for where the "must be above +// MinDelegatorStk" rule is now enforced. +fn can_schedule_revoke_delegation_below_min_delegator_stake() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 8), (3, 20)]) + .with_candidates(vec![(1, 20), (3, 20)]) + .with_delegations(vec![(2, 1, 5), (2, 3, 3)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + }); +} + +// DELEGATOR BOND MORE + +#[test] +fn delegator_bond_more_reserves_balance() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 5); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 0); + }); +} + +#[test] +fn delegator_bond_more_increases_total_staked() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::total(), 40); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + assert_eq!(ParachainStaking::total(), 45); + }); +} + +#[test] +fn delegator_bond_more_updates_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::delegator_state(2).expect("exists").total(), 10); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + assert_eq!(ParachainStaking::delegator_state(2).expect("exists").total(), 15); + }); +} + +#[test] +fn delegator_bond_more_updates_candidate_state_top_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].owner, 2); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].amount, 10); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().total, 10); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].owner, 2); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].amount, 15); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().total, 15); + }); +} + +#[test] +fn delegator_bond_more_updates_candidate_state_bottom_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10), (3, 1, 20), (4, 1, 20), (5, 1, 20), (6, 1, 20)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::bottom_delegations(1).expect("exists").delegations[0].owner, 2); + assert_eq!(ParachainStaking::bottom_delegations(1).expect("exists").delegations[0].amount, 10); + assert_eq!(ParachainStaking::bottom_delegations(1).unwrap().total, 10); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + assert_events_eq!(Event::DelegationIncreased { delegator: 2, candidate: 1, amount: 5, in_top: false }); + assert_eq!(ParachainStaking::bottom_delegations(1).expect("exists").delegations[0].owner, 2); + assert_eq!(ParachainStaking::bottom_delegations(1).expect("exists").delegations[0].amount, 15); + assert_eq!(ParachainStaking::bottom_delegations(1).unwrap().total, 15); + }); +} + +#[test] +fn delegator_bond_more_increases_total() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::total(), 40); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + assert_eq!(ParachainStaking::total(), 45); + }); +} + +#[test] +fn can_delegator_bond_more_for_leaving_candidate() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + }); +} + +#[test] +fn delegator_bond_more_disallowed_when_revoke_scheduled() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_noop!( + ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5), + >::PendingDelegationRevoke + ); + }); +} + +#[test] +fn delegator_bond_more_allowed_when_bond_decrease_scheduled() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 15)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5,)); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 1, 5)); + }); +} + +// DELEGATOR BOND LESS + +#[test] +fn delegator_bond_less_event_emits_correctly() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + assert_events_eq!(Event::DelegationDecreaseScheduled { + delegator: 2, + candidate: 1, + amount_to_decrease: 5, + execute_round: 3, + }); + }); +} + +#[test] +fn delegator_bond_less_updates_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + let state = ParachainStaking::delegation_scheduled_requests(&1); + assert_eq!( + state, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Decrease(5) }], + ); + }); +} + +#[test] +fn delegator_not_allowed_bond_less_if_leaving() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 1), + >::PendingDelegationRequestAlreadyExists, + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_if_revoking() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 1), + Error::::PendingDelegationRequestAlreadyExists + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_if_not_delegator() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5), + Error::::DelegatorDNE + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_if_candidate_dne() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 3, 5), + Error::::DelegationDNE + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_if_delegation_dne() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 3, 5), + Error::::DelegationDNE + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_below_min_collator_stk() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 6), + Error::::DelegatorBondBelowMin + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_more_than_total_delegation() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 11), + Error::::DelegatorBondBelowMin + ); + }); +} + +#[test] +fn cannot_delegator_bond_less_below_min_delegation() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 8), + Error::::DelegationBelowMin + ); + }); +} + +// EXECUTE PENDING DELEGATION REQUEST + +// 1. REVOKE DELEGATION + +#[test] +fn execute_revoke_delegation_emits_exit_event_if_exit_happens() { + // last delegation is revocation + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_events_emitted!(Event::DelegatorLeftCandidate { + delegator: 2, + candidate: 1, + unstaked_amount: 10, + total_candidate_staked: 30 + }); + assert_events_emitted!(Event::DelegatorLeft { delegator: 2, unstaked_amount: 10 }); + }); +} + +#[test] +fn cannot_execute_revoke_delegation_below_min_delegator_stake() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 8), (3, 20)]) + .with_candidates(vec![(1, 20), (3, 20)]) + .with_delegations(vec![(2, 1, 5), (2, 3, 3)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_noop!( + ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1), + Error::::DelegatorBondBelowMin + ); + // but delegator can cancel the request and request to leave instead: + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(20); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 2)); + }); +} + +#[test] +fn revoke_delegation_executes_exit_if_last_delegation() { + // last delegation is revocation + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_events_emitted!(Event::DelegatorLeftCandidate { + delegator: 2, + candidate: 1, + unstaked_amount: 10, + total_candidate_staked: 30 + }); + assert_events_emitted!(Event::DelegatorLeft { delegator: 2, unstaked_amount: 10 }); + }); +} + +#[test] +fn execute_revoke_delegation_emits_correct_event() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_events_emitted!(Event::DelegatorLeftCandidate { + delegator: 2, + candidate: 1, + unstaked_amount: 10, + total_candidate_staked: 30 + }); + }); +} + +#[test] +fn execute_revoke_delegation_unreserves_balance() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 0); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 10); + }); +} + +#[test] +fn execute_revoke_delegation_adds_revocation_to_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert!(!ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2)); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert!(ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2)); + }); +} + +#[test] +fn execute_revoke_delegation_removes_revocation_from_delegator_state_upon_execution() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert!(!ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2)); + }); +} + +#[test] +fn execute_revoke_delegation_removes_revocation_from_state_for_single_delegation_leave() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert!( + !ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2), + "delegation was not removed" + ); + }); +} + +#[test] +fn execute_revoke_delegation_decreases_total_staked() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::total(), 40); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::total(), 30); + }); +} + +#[test] +fn execute_revoke_delegation_for_last_delegation_removes_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert!(ParachainStaking::delegator_state(2).is_some()); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + // this will be confusing for people + // if status is leaving, then execute_delegation_request works if last delegation + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert!(ParachainStaking::delegator_state(2).is_none()); + }); +} + +#[test] +fn execute_revoke_delegation_removes_delegation_from_candidate_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::candidate_info(1).expect("exists").delegation_count, 1u32); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert!(ParachainStaking::candidate_info(1).expect("exists").delegation_count.is_zero()); + }); +} + +#[test] +fn can_execute_revoke_delegation_for_leaving_candidate() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + // can execute delegation request for leaving candidate + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + }); +} + +#[test] +fn can_execute_leave_candidates_if_revoking_candidate() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + // revocation executes during execute leave candidates (callable by anyone) + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 1)); + assert!(!ParachainStaking::is_delegator(&2)); + assert_eq!(Balances::balance_on_hold(&HoldReason::StakingDelegator.into(), &2), 0); + assert_eq!(Balances::reducible_balance(&2, Preservation::Preserve, Fortitude::Force), 10); + }); +} + +#[test] +fn delegator_bond_more_after_revoke_delegation_does_not_effect_exit() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 30), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(2), 3, 10)); + roll_to(100); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert!(ParachainStaking::is_delegator(&2)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 10); + }); +} + +#[test] +fn delegator_bond_less_after_revoke_delegation_does_not_effect_exit() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 30), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_events_eq!(Event::DelegationRevocationScheduled { + round: 1, + delegator: 2, + candidate: 1, + scheduled_exit: 3, + }); + assert_noop!( + ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 2), + Error::::PendingDelegationRequestAlreadyExists + ); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 3, 2)); + roll_to(10); + roll_blocks(1); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 3)); + assert_events_eq!( + Event::DelegatorLeftCandidate { + delegator: 2, + candidate: 1, + unstaked_amount: 10, + total_candidate_staked: 30, + }, + Event::DelegationRevoked { delegator: 2, candidate: 1, unstaked_amount: 10 }, + Event::DelegationDecreased { delegator: 2, candidate: 3, amount: 2, in_top: true }, + ); + assert!(ParachainStaking::is_delegator(&2)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 22); + }); +} + +// 2. EXECUTE BOND LESS + +#[test] +fn execute_delegator_bond_less_unreserves_balance() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 0); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 5); + }); +} + +#[test] +fn execute_delegator_bond_less_decreases_total_staked() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::total(), 40); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::total(), 35); + }); +} + +#[test] +fn execute_delegator_bond_less_updates_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::delegator_state(2).expect("exists").total(), 10); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::delegator_state(2).expect("exists").total(), 5); + }); +} + +#[test] +fn execute_delegator_bond_less_updates_candidate_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].owner, 2); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].amount, 10); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].owner, 2); + assert_eq!(ParachainStaking::top_delegations(1).unwrap().delegations[0].amount, 5); + }); +} + +#[test] +fn execute_delegator_bond_less_decreases_total() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_eq!(ParachainStaking::total(), 40); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!(ParachainStaking::total(), 35); + }); +} + +#[test] +fn execute_delegator_bond_less_updates_just_bottom_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 10), (3, 11), (4, 12), (5, 14), (6, 15)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10), (3, 1, 11), (4, 1, 12), (5, 1, 14), (6, 1, 15)]) + .build() + .execute_with(|| { + let pre_call_candidate_info = ParachainStaking::candidate_info(&1).expect("delegated by all so exists"); + let pre_call_top_delegations = ParachainStaking::top_delegations(&1).expect("delegated by all so exists"); + let pre_call_bottom_delegations = + ParachainStaking::bottom_delegations(&1).expect("delegated by all so exists"); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 2)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + let post_call_candidate_info = ParachainStaking::candidate_info(&1).expect("delegated by all so exists"); + let post_call_top_delegations = ParachainStaking::top_delegations(&1).expect("delegated by all so exists"); + let post_call_bottom_delegations = + ParachainStaking::bottom_delegations(&1).expect("delegated by all so exists"); + let mut not_equal = false; + for Bond { owner, amount } in pre_call_bottom_delegations.delegations { + for Bond { owner: post_owner, amount: post_amount } in &post_call_bottom_delegations.delegations { + if &owner == post_owner { + if &amount != post_amount { + not_equal = true; + break; + } + } + } + } + assert!(not_equal); + let mut equal = true; + for Bond { owner, amount } in pre_call_top_delegations.delegations { + for Bond { owner: post_owner, amount: post_amount } in &post_call_top_delegations.delegations { + if &owner == post_owner { + if &amount != post_amount { + equal = false; + break; + } + } + } + } + assert!(equal); + assert_eq!(pre_call_candidate_info.total_counted, post_call_candidate_info.total_counted); + }); +} + +#[test] +fn execute_delegator_bond_less_does_not_delete_bottom_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 10), (3, 11), (4, 12), (5, 14), (6, 15)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10), (3, 1, 11), (4, 1, 12), (5, 1, 14), (6, 1, 15)]) + .build() + .execute_with(|| { + let pre_call_candidate_info = ParachainStaking::candidate_info(&1).expect("delegated by all so exists"); + let pre_call_top_delegations = ParachainStaking::top_delegations(&1).expect("delegated by all so exists"); + let pre_call_bottom_delegations = + ParachainStaking::bottom_delegations(&1).expect("delegated by all so exists"); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(6), 1, 4)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(6), 6, 1)); + let post_call_candidate_info = ParachainStaking::candidate_info(&1).expect("delegated by all so exists"); + let post_call_top_delegations = ParachainStaking::top_delegations(&1).expect("delegated by all so exists"); + let post_call_bottom_delegations = + ParachainStaking::bottom_delegations(&1).expect("delegated by all so exists"); + let mut equal = true; + for Bond { owner, amount } in pre_call_bottom_delegations.delegations { + for Bond { owner: post_owner, amount: post_amount } in &post_call_bottom_delegations.delegations { + if &owner == post_owner { + if &amount != post_amount { + equal = false; + break; + } + } + } + } + assert!(equal); + let mut not_equal = false; + for Bond { owner, amount } in pre_call_top_delegations.delegations { + for Bond { owner: post_owner, amount: post_amount } in &post_call_top_delegations.delegations { + if &owner == post_owner { + if &amount != post_amount { + not_equal = true; + break; + } + } + } + } + assert!(not_equal); + assert_eq!(pre_call_candidate_info.total_counted - 4, post_call_candidate_info.total_counted); + }); +} + +#[test] +fn can_execute_delegator_bond_less_for_leaving_candidate() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 15)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 1)); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + roll_to(10); + // can execute bond more delegation request for leaving candidate + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + }); +} + +// CANCEL PENDING DELEGATION REQUEST +// 1. CANCEL REVOKE DELEGATION + +#[test] +fn cancel_revoke_delegation_emits_correct_event() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + assert_events_emitted!(Event::CancelledDelegationRequest { + delegator: 2, + collator: 1, + cancelled_request: CancelledScheduledRequest { + when_executable: 3, + action: DelegationAction::Revoke(10), + }, + }); + }); +} + +#[test] +fn cancel_revoke_delegation_updates_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 10)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + let state = ParachainStaking::delegation_scheduled_requests(&1); + assert_eq!( + state, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Revoke(10) }], + ); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 10 + ); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + assert!(!ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 0 + ); + }); +} + +// 2. CANCEL DELEGATOR BOND LESS + +#[test] +fn cancel_delegator_bond_less_correct_event() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 15)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + assert_events_emitted!(Event::CancelledDelegationRequest { + delegator: 2, + collator: 1, + cancelled_request: CancelledScheduledRequest { + when_executable: 3, + action: DelegationAction::Decrease(5), + }, + }); + }); +} + +#[test] +fn cancel_delegator_bond_less_updates_delegator_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 15)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 15)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 5)); + let state = ParachainStaking::delegation_scheduled_requests(&1); + assert_eq!( + state, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Decrease(5) }], + ); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 5 + ); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + assert!(!ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 0 + ); + }); +} + +// ~~ PROPERTY-BASED TESTS ~~ + +#[test] +fn delegator_schedule_revocation_total() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20), (5, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20), (5, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10), (2, 4, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 10 + ); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 0 + ); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 5, 10, 0, 2)); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 3)); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 4)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 20, + ); + roll_to(20); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 3)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 10, + ); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 4)); + assert_eq!( + ParachainStaking::delegator_state(&2).map(|x| x.less_total).expect("delegator state must exist"), + 0 + ); + }); +} + +#[test] +fn paid_collator_commission_matches_config() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10), (3, 1, 10)]) + .build() + .execute_with(|| { + roll_to_round_begin(2); + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(4), 20u128, 100u32)); + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 40 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 1, total_balance: 40 }, + Event::JoinedCollatorCandidates { account: 4, amount_locked: 20, new_total_amt_locked: 60 }, + ); + + roll_blocks(1); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(5), 4, 10, 10, 10)); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(6), 4, 10, 10, 10)); + assert_events_eq!( + Event::Delegation { + delegator: 5, + locked_amount: 10, + candidate: 4, + delegator_position: DelegatorAdded::AddedToTop { new_total: 30 }, + auto_compound: Percent::zero(), + }, + Event::Delegation { + delegator: 6, + locked_amount: 10, + candidate: 4, + delegator_position: DelegatorAdded::AddedToTop { new_total: 40 }, + auto_compound: Percent::zero(), + }, + ); + + roll_to_round_begin(3); + assert_events_eq!( + Event::CollatorChosen { round: 3, collator_account: 1, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 3, collator_account: 4, total_exposed_amount: 40 }, + Event::NewRound { starting_block: 10, round: 3, selected_collators_number: 2, total_balance: 80 }, + ); + // only reward author with id 4 + set_author(3, 4, 100); + roll_to_round_begin(5); + // 20% of 10 is commission + due_portion (0) = 2 + 4 = 6 + // all delegator payouts are 10-2 = 8 * stake_pct + assert_events_eq!( + Event::CollatorChosen { round: 5, collator_account: 1, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 5, collator_account: 4, total_exposed_amount: 40 }, + Event::NewRound { starting_block: 20, round: 5, selected_collators_number: 2, total_balance: 80 }, + ); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 4, rewards: 27 }, + Event::Rewarded { account: 5, rewards: 9 }, + Event::Rewarded { account: 6, rewards: 9 }, + ); + }); +} + +#[test] +fn collator_exit_executes_after_delay() { + ExtBuilder::default() + .with_balances(vec![(1, 1000), (2, 300), (3, 100), (4, 100), (5, 100), (6, 100), (7, 100), (8, 9), (9, 4)]) + .with_candidates(vec![(1, 500), (2, 200)]) + .with_delegations(vec![(3, 1, 100), (4, 1, 100), (5, 2, 100), (6, 2, 100)]) + .build() + .execute_with(|| { + roll_to(11); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(2), 2)); + assert_events_eq!(Event::CandidateScheduledExit { exit_allowed_round: 3, candidate: 2, scheduled_exit: 5 }); + let info = ParachainStaking::candidate_info(&2).unwrap(); + assert_eq!(info.status, CollatorStatus::Leaving(5)); + roll_to(21); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(2), 2, 2)); + // we must exclude leaving collators from rewards while + // holding them retroactively accountable for previous faults + // (within the last T::SlashingWindow blocks) + assert_events_eq!(Event::CandidateLeft { + ex_candidate: 2, + unlocked_amount: 400, + new_total_amt_locked: 700, + },); + }); +} + +#[test] +fn collator_selection_chooses_top_candidates() { + ExtBuilder::default() + .with_balances(vec![ + (1, 1000), + (2, 1000), + (3, 1000), + (4, 1000), + (5, 1000), + (6, 1000), + (7, 33), + (8, 33), + (9, 33), + ]) + .with_candidates(vec![(1, 100), (2, 90), (3, 80), (4, 70), (5, 60), (6, 50)]) + .build() + .execute_with(|| { + roll_to_round_begin(2); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(6), 6)); + // should choose top TotalSelectedCandidates (5), in order + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 100 }, + Event::CollatorChosen { round: 2, collator_account: 2, total_exposed_amount: 90 }, + Event::CollatorChosen { round: 2, collator_account: 3, total_exposed_amount: 80 }, + Event::CollatorChosen { round: 2, collator_account: 4, total_exposed_amount: 70 }, + Event::CollatorChosen { round: 2, collator_account: 5, total_exposed_amount: 60 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 5, total_balance: 400 }, + Event::CandidateScheduledExit { exit_allowed_round: 2, candidate: 6, scheduled_exit: 4 }, + ); + roll_to_round_begin(4); + roll_blocks(1); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(6), 6, 0)); + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(6), 69u128, 100u32)); + assert_events_eq!( + Event::CandidateLeft { ex_candidate: 6, unlocked_amount: 50, new_total_amt_locked: 400 }, + Event::JoinedCollatorCandidates { account: 6, amount_locked: 69u128, new_total_amt_locked: 469u128 }, + ); + roll_to_round_begin(6); + // should choose top TotalSelectedCandidates (5), in order + assert_events_eq!( + Event::CollatorChosen { round: 6, collator_account: 1, total_exposed_amount: 100 }, + Event::CollatorChosen { round: 6, collator_account: 2, total_exposed_amount: 90 }, + Event::CollatorChosen { round: 6, collator_account: 3, total_exposed_amount: 80 }, + Event::CollatorChosen { round: 6, collator_account: 4, total_exposed_amount: 70 }, + Event::CollatorChosen { round: 6, collator_account: 6, total_exposed_amount: 69 }, + Event::NewRound { starting_block: 25, round: 6, selected_collators_number: 5, total_balance: 409 }, + ); + }); +} + +#[test] +fn payout_distribution_to_solo_collators() { + ExtBuilder::default() + .with_balances(vec![ + (1, 1000), + (2, 1000), + (3, 1000), + (4, 1000), + (7, 33), + (8, 33), + (9, 33), + (crate::mock::BlockchainOperationTreasury::get(), 1000), + ]) + .with_candidates(vec![(1, 100), (2, 90), (3, 80), (4, 70)]) + .build() + .execute_with(|| { + roll_to_round_begin(2); + // should choose top TotalCandidatesSelected (5), in order + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 100 }, + Event::CollatorChosen { round: 2, collator_account: 2, total_exposed_amount: 90 }, + Event::CollatorChosen { round: 2, collator_account: 3, total_exposed_amount: 80 }, + Event::CollatorChosen { round: 2, collator_account: 4, total_exposed_amount: 70 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 4, total_balance: 340 }, + ); + // ~ set block author as 1 for all blocks this round + set_author(2, 1, 100); + roll_to_round_begin(4); + assert_events_eq!( + Event::CollatorChosen { round: 4, collator_account: 1, total_exposed_amount: 100 }, + Event::CollatorChosen { round: 4, collator_account: 2, total_exposed_amount: 90 }, + Event::CollatorChosen { round: 4, collator_account: 3, total_exposed_amount: 80 }, + Event::CollatorChosen { round: 4, collator_account: 4, total_exposed_amount: 70 }, + Event::NewRound { starting_block: 15, round: 4, selected_collators_number: 4, total_balance: 340 }, + ); + // pay total issuance to 1 at 2nd block + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 255 }); + // ~ set block author as 1 for 3 blocks this round + set_author(4, 1, 60); + // ~ set block author as 2 for 2 blocks this round + set_author(4, 2, 40); + roll_to_round_begin(6); + // pay 60% total issuance to 1 and 40% total issuance to 2 + assert_events_eq!( + Event::CollatorChosen { round: 6, collator_account: 1, total_exposed_amount: 100 }, + Event::CollatorChosen { round: 6, collator_account: 2, total_exposed_amount: 90 }, + Event::CollatorChosen { round: 6, collator_account: 3, total_exposed_amount: 80 }, + Event::CollatorChosen { round: 6, collator_account: 4, total_exposed_amount: 70 }, + Event::NewRound { starting_block: 25, round: 6, selected_collators_number: 4, total_balance: 340 }, + ); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 153 }); + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 2, rewards: 102 },); + // ~ each collator produces 1 block this round + set_author(6, 1, 20); + set_author(6, 2, 20); + set_author(6, 3, 20); + set_author(6, 4, 20); + roll_to_round_begin(8); + // pay 20% issuance for all collators + assert_events_eq!( + Event::CollatorChosen { round: 8, collator_account: 1, total_exposed_amount: 100 }, + Event::CollatorChosen { round: 8, collator_account: 2, total_exposed_amount: 90 }, + Event::CollatorChosen { round: 8, collator_account: 3, total_exposed_amount: 80 }, + Event::CollatorChosen { round: 8, collator_account: 4, total_exposed_amount: 70 }, + Event::NewRound { starting_block: 35, round: 8, selected_collators_number: 4, total_balance: 340 }, + ); + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 3, rewards: 64 }); + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 4, rewards: 64 }); + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 64 }); + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 2, rewards: 64 }); + // check that distributing rewards clears awarded pts + assert!(ParachainStaking::awarded_pts(1, 1).is_zero()); + assert!(ParachainStaking::awarded_pts(4, 1).is_zero()); + assert!(ParachainStaking::awarded_pts(4, 2).is_zero()); + assert!(ParachainStaking::awarded_pts(6, 1).is_zero()); + assert!(ParachainStaking::awarded_pts(6, 2).is_zero()); + assert!(ParachainStaking::awarded_pts(6, 3).is_zero()); + assert!(ParachainStaking::awarded_pts(6, 4).is_zero()); + }); +} + +#[test] +fn multiple_delegations() { + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + ]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 10)]) + .with_delegations(vec![(6, 1, 10), (7, 1, 10), (8, 2, 10), (9, 2, 10), (10, 1, 10)]) + .build() + .execute_with(|| { + roll_to_round_begin(2); + // chooses top TotalSelectedCandidates (5), in order + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 2, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 2, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 2, collator_account: 4, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 2, collator_account: 5, total_exposed_amount: 10 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 5, total_balance: 140 }, + ); + roll_blocks(1); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(6), 2, 10, 10, 10)); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(6), 3, 10, 10, 10)); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(6), 4, 10, 10, 10)); + assert_events_eq!( + Event::Delegation { + delegator: 6, + locked_amount: 10, + candidate: 2, + delegator_position: DelegatorAdded::AddedToTop { new_total: 50 }, + auto_compound: Percent::zero(), + }, + Event::Delegation { + delegator: 6, + locked_amount: 10, + candidate: 3, + delegator_position: DelegatorAdded::AddedToTop { new_total: 30 }, + auto_compound: Percent::zero(), + }, + Event::Delegation { + delegator: 6, + locked_amount: 10, + candidate: 4, + delegator_position: DelegatorAdded::AddedToTop { new_total: 30 }, + auto_compound: Percent::zero(), + }, + ); + roll_to_round_begin(6); + roll_blocks(1); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(7), 2, 80, 10, 10)); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(10), 2, 10, 10, 10)); + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(2), 5)); + assert_events_eq!( + Event::Delegation { + delegator: 7, + locked_amount: 80, + candidate: 2, + delegator_position: DelegatorAdded::AddedToTop { new_total: 130 }, + auto_compound: Percent::zero(), + }, + Event::Delegation { + delegator: 10, + locked_amount: 10, + candidate: 2, + delegator_position: DelegatorAdded::AddedToBottom, + auto_compound: Percent::zero(), + }, + Event::CandidateScheduledExit { exit_allowed_round: 6, candidate: 2, scheduled_exit: 8 }, + ); + roll_to_round_begin(7); + assert_events_eq!( + Event::CollatorChosen { round: 7, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 7, collator_account: 3, total_exposed_amount: 30 }, + Event::CollatorChosen { round: 7, collator_account: 4, total_exposed_amount: 30 }, + Event::CollatorChosen { round: 7, collator_account: 5, total_exposed_amount: 10 }, + Event::NewRound { starting_block: 30, round: 7, selected_collators_number: 4, total_balance: 120 }, + ); + // verify that delegations are removed after collator leaves, not before + assert_eq!(ParachainStaking::delegator_state(7).unwrap().total(), 90); + assert_eq!(ParachainStaking::delegator_state(7).unwrap().delegations.0.len(), 2usize); + assert_eq!(ParachainStaking::delegator_state(6).unwrap().total(), 40); + assert_eq!(ParachainStaking::delegator_state(6).unwrap().delegations.0.len(), 4usize); + assert_eq!(Balances::balance_on_hold(&HoldReason::StakingDelegator.into(), &6), 40); + assert_eq!(Balances::balance_on_hold(&HoldReason::StakingDelegator.into(), &7), 90); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&6), 60); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&7), 10); + roll_to_round_begin(8); + roll_blocks(1); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(2), 2, 5)); + assert_events_eq!(Event::CandidateLeft { + ex_candidate: 2, + unlocked_amount: 140, + new_total_amt_locked: 120, + }); + assert_eq!(ParachainStaking::delegator_state(7).unwrap().total(), 10); + assert_eq!(ParachainStaking::delegator_state(6).unwrap().total(), 30); + assert_eq!(ParachainStaking::delegator_state(7).unwrap().delegations.0.len(), 1usize); + assert_eq!(ParachainStaking::delegator_state(6).unwrap().delegations.0.len(), 3usize); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&6), 70); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&7), 90); + }); +} + +#[test] +// The test verifies that the pending revoke request is removed by 2's exit so there is no dangling +// revoke request after 2 exits +fn execute_leave_candidate_removes_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 100), (3, 100), (4, 100)]) + .with_candidates(vec![(1, 20), (2, 20)]) + .with_delegations(vec![(3, 1, 10), (3, 2, 10), (4, 1, 10), (4, 2, 10)]) + .build() + .execute_with(|| { + // Verifies the revocation request is initially empty + assert!(!ParachainStaking::delegation_scheduled_requests(&2).iter().any(|x| x.delegator == 3)); + + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(2), 2)); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(3), 2)); + // Verifies the revocation request is present + assert!(ParachainStaking::delegation_scheduled_requests(&2).iter().any(|x| x.delegator == 3)); + + roll_to(16); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(2), 2, 2)); + // Verifies the revocation request is again empty + assert!(!ParachainStaking::delegation_scheduled_requests(&2).iter().any(|x| x.delegator == 3)); + }); +} + +#[test] +fn payouts_follow_delegation_changes() { + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + (crate::mock::BlockchainOperationTreasury::get(), 1000), + ]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(6, 1, 10), (7, 1, 10), (8, 2, 10), (9, 2, 10), (10, 1, 10)]) + .build() + .execute_with(|| { + roll_to_round_begin(2); + // chooses top TotalSelectedCandidates (5), in order + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 2, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 2, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 2, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 4, total_balance: 130 }, + ); + // ~ set block author as 1 for all blocks this round + set_author(2, 1, 100); + roll_to_round_begin(4); + // distribute total issuance to collator 1 and its delegators 6, 7, 19 + assert_events_eq!( + Event::CollatorChosen { round: 4, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 4, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 4, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 4, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 15, round: 4, selected_collators_number: 4, total_balance: 130 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 49 }, + Event::Rewarded { account: 6, rewards: 15 }, + Event::Rewarded { account: 7, rewards: 15 }, + Event::Rewarded { account: 10, rewards: 15 }, + ); + // ~ set block author as 1 for all blocks this round + set_author(3, 1, 100); + set_author(4, 1, 100); + set_author(5, 1, 100); + set_author(6, 1, 100); + + roll_blocks(1); + // 1. ensure delegators are paid for 2 rounds after they leave + assert_noop!( + ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(66)), + Error::::DelegatorDNE + ); + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(6))); + assert_events_eq!(Event::DelegatorExitScheduled { round: 4, delegator: 6, scheduled_exit: 6 }); + // fast forward to block in which delegator 6 exit executes + roll_to_round_begin(5); + assert_events_eq!( + Event::CollatorChosen { round: 5, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 5, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 5, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 5, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 20, round: 5, selected_collators_number: 4, total_balance: 130 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 49 }, + Event::Rewarded { account: 6, rewards: 15 }, + Event::Rewarded { account: 7, rewards: 15 }, + Event::Rewarded { account: 10, rewards: 15 }, + ); + // keep paying 6 (note: inflation is in terms of total issuance so that's why 1 is 21) + roll_to_round_begin(6); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(6), 6, 10)); + assert_events_eq!( + Event::CollatorChosen { round: 6, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 6, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 6, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 6, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 25, round: 6, selected_collators_number: 4, total_balance: 130 }, + Event::DelegatorLeftCandidate { + delegator: 6, + candidate: 1, + unstaked_amount: 10, + total_candidate_staked: 40, + }, + Event::DelegatorLeft { delegator: 6, unstaked_amount: 10 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 49 }, + Event::Rewarded { account: 6, rewards: 15 }, + Event::Rewarded { account: 7, rewards: 15 }, + Event::Rewarded { account: 10, rewards: 15 }, + ); + // 6 won't be paid for this round because they left already + set_author(7, 1, 100); + roll_to_round_begin(7); + // keep paying 6 + assert_events_eq!( + Event::CollatorChosen { round: 7, collator_account: 1, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 7, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 7, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 7, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 30, round: 7, selected_collators_number: 4, total_balance: 120 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 57 }, + Event::Rewarded { account: 7, rewards: 19 }, + Event::Rewarded { account: 10, rewards: 19 }, + ); + roll_to_round_begin(8); + assert_events_eq!( + Event::CollatorChosen { round: 8, collator_account: 1, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 8, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 8, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 8, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 35, round: 8, selected_collators_number: 4, total_balance: 120 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 57 }, + Event::Rewarded { account: 7, rewards: 19 }, + Event::Rewarded { account: 10, rewards: 19 }, + ); + set_author(8, 1, 100); + roll_to_round_begin(9); + // no more paying 6 + assert_events_eq!( + Event::CollatorChosen { round: 9, collator_account: 1, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 9, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 9, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 9, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 40, round: 9, selected_collators_number: 4, total_balance: 120 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 57 }, + Event::Rewarded { account: 7, rewards: 19 }, + Event::Rewarded { account: 10, rewards: 19 }, + ); + roll_blocks(1); + set_author(9, 1, 100); + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(8), 1, 10, 10, 10)); + assert_events_eq!(Event::Delegation { + delegator: 8, + locked_amount: 10, + candidate: 1, + delegator_position: DelegatorAdded::AddedToTop { new_total: 50 }, + auto_compound: Percent::zero(), + }); + + roll_to_round_begin(10); + // new delegation is not rewarded yet + assert_events_eq!( + Event::CollatorChosen { round: 10, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 10, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 10, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 10, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 45, round: 10, selected_collators_number: 4, total_balance: 130 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 57 }, + Event::Rewarded { account: 7, rewards: 19 }, + Event::Rewarded { account: 10, rewards: 19 }, + ); + set_author(10, 1, 100); + roll_to_round_begin(11); + // new delegation not rewarded yet + assert_events_eq!( + Event::CollatorChosen { round: 11, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 11, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 11, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 11, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 50, round: 11, selected_collators_number: 4, total_balance: 130 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 57 }, + Event::Rewarded { account: 7, rewards: 19 }, + Event::Rewarded { account: 10, rewards: 19 }, + ); + roll_to_round_begin(12); + // new delegation is rewarded for first time + // 2 rounds after joining (`RewardPaymentDelay` = 2) + assert_events_eq!( + Event::CollatorChosen { round: 12, collator_account: 1, total_exposed_amount: 50 }, + Event::CollatorChosen { round: 12, collator_account: 2, total_exposed_amount: 40 }, + Event::CollatorChosen { round: 12, collator_account: 3, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 12, collator_account: 4, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 55, round: 12, selected_collators_number: 4, total_balance: 130 }, + ); + roll_blocks(3); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 49 }, + Event::Rewarded { account: 7, rewards: 15 }, + Event::Rewarded { account: 10, rewards: 15 }, + Event::Rewarded { account: 8, rewards: 15 }, + ); + }); +} + +#[test] +fn bottom_delegations_are_empty_when_top_delegations_not_full() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 10), (3, 10), (4, 10), (5, 10)]) + .with_candidates(vec![(1, 20)]) + .build() + .execute_with(|| { + // no top delegators => no bottom delegators + let top_delegations = ParachainStaking::top_delegations(1).unwrap(); + let bottom_delegations = ParachainStaking::bottom_delegations(1).unwrap(); + assert!(top_delegations.delegations.is_empty()); + assert!(bottom_delegations.delegations.is_empty()); + // 1 delegator => 1 top delegator, 0 bottom delegators + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 10, 10)); + let top_delegations = ParachainStaking::top_delegations(1).unwrap(); + let bottom_delegations = ParachainStaking::bottom_delegations(1).unwrap(); + assert_eq!(top_delegations.delegations.len(), 1usize); + assert!(bottom_delegations.delegations.is_empty()); + // 2 delegators => 2 top delegators, 0 bottom delegators + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(3), 1, 10, 10, 10)); + let top_delegations = ParachainStaking::top_delegations(1).unwrap(); + let bottom_delegations = ParachainStaking::bottom_delegations(1).unwrap(); + assert_eq!(top_delegations.delegations.len(), 2usize); + assert!(bottom_delegations.delegations.is_empty()); + // 3 delegators => 3 top delegators, 0 bottom delegators + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(4), 1, 10, 10, 10)); + let top_delegations = ParachainStaking::top_delegations(1).unwrap(); + let bottom_delegations = ParachainStaking::bottom_delegations(1).unwrap(); + assert_eq!(top_delegations.delegations.len(), 3usize); + assert!(bottom_delegations.delegations.is_empty()); + // 4 delegators => 4 top delegators, 0 bottom delegators + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(5), 1, 10, 10, 10)); + let top_delegations = ParachainStaking::top_delegations(1).unwrap(); + let bottom_delegations = ParachainStaking::bottom_delegations(1).unwrap(); + assert_eq!(top_delegations.delegations.len(), 4usize); + assert!(bottom_delegations.delegations.is_empty()); + }); +} + +#[test] +fn candidate_pool_updates_when_total_counted_changes() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (3, 19), (4, 20), (5, 21), (6, 22), (7, 15), (8, 16), (9, 17), (10, 18)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![ + (3, 1, 11), + (4, 1, 12), + (5, 1, 13), + (6, 1, 14), + (7, 1, 15), + (8, 1, 16), + (9, 1, 17), + (10, 1, 18), + ]) + .build() + .execute_with(|| { + fn is_candidate_pool_bond(account: u64, bond: u128) { + let pool = ParachainStaking::candidate_pool(); + for candidate in pool.0 { + if candidate.owner == account { + assert_eq!( + candidate.amount, bond, + "Candidate Bond {:?} is Not Equal to Expected: {:?}", + candidate.amount, bond + ); + } + } + } + // 15 + 16 + 17 + 18 + 20 = 86 (top 4 + self bond) + is_candidate_pool_bond(1, 86); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(3), 1, 8)); + // 3: 11 -> 19 => 3 is in top, bumps out 7 + // 16 + 17 + 18 + 19 + 20 = 90 (top 4 + self bond) + is_candidate_pool_bond(1, 90); + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(4), 1, 8)); + // 4: 12 -> 20 => 4 is in top, bumps out 8 + // 17 + 18 + 19 + 20 + 20 = 94 (top 4 + self bond) + is_candidate_pool_bond(1, 94); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(10), 1, 3)); + roll_to(30); + // 10: 18 -> 15 => 10 bumped to bottom, 8 bumped to top (- 18 + 16 = -2 for count) + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(10), 10, 1)); + // 16 + 17 + 19 + 20 + 20 = 92 (top 4 + self bond) + is_candidate_pool_bond(1, 92); + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(9), 1, 4)); + roll_to(40); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(9), 9, 1)); + // 15 + 16 + 19 + 20 + 20 = 90 (top 4 + self bond) + is_candidate_pool_bond(1, 90); + }); +} + +#[test] +fn only_top_collators_are_counted() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (3, 19), (4, 20), (5, 21), (6, 22), (7, 15), (8, 16), (9, 17), (10, 18)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![ + (3, 1, 11), + (4, 1, 12), + (5, 1, 13), + (6, 1, 14), + (7, 1, 15), + (8, 1, 16), + (9, 1, 17), + (10, 1, 18), + ]) + .build() + .execute_with(|| { + // sanity check that 3-10 are delegators immediately + for i in 3..11 { + assert!(ParachainStaking::is_delegator(&i)); + } + let collator_state = ParachainStaking::candidate_info(1).unwrap(); + // 15 + 16 + 17 + 18 + 20 = 86 (top 4 + self bond) + assert_eq!(collator_state.total_counted, 86); + // bump bottom to the top + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(3), 1, 8)); + assert_events_emitted!(Event::DelegationIncreased { delegator: 3, candidate: 1, amount: 8, in_top: true }); + let collator_state = ParachainStaking::candidate_info(1).unwrap(); + // 16 + 17 + 18 + 19 + 20 = 90 (top 4 + self bond) + assert_eq!(collator_state.total_counted, 90); + // bump bottom to the top + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(4), 1, 8)); + assert_events_emitted!(Event::DelegationIncreased { delegator: 4, candidate: 1, amount: 8, in_top: true }); + let collator_state = ParachainStaking::candidate_info(1).unwrap(); + // 17 + 18 + 19 + 20 + 20 = 94 (top 4 + self bond) + assert_eq!(collator_state.total_counted, 94); + // bump bottom to the top + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(5), 1, 8)); + assert_events_emitted!(Event::DelegationIncreased { delegator: 5, candidate: 1, amount: 8, in_top: true }); + let collator_state = ParachainStaking::candidate_info(1).unwrap(); + // 18 + 19 + 20 + 21 + 20 = 98 (top 4 + self bond) + assert_eq!(collator_state.total_counted, 98); + // bump bottom to the top + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(6), 1, 8)); + assert_events_emitted!(Event::DelegationIncreased { delegator: 6, candidate: 1, amount: 8, in_top: true }); + let collator_state = ParachainStaking::candidate_info(1).unwrap(); + // 19 + 20 + 21 + 22 + 20 = 102 (top 4 + self bond) + assert_eq!(collator_state.total_counted, 102); + }); +} + +#[test] +fn delegation_events_convey_correct_position() { + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + ]) + .with_candidates(vec![(1, 20), (2, 20)]) + .with_delegations(vec![(3, 1, 11), (4, 1, 12), (5, 1, 13), (6, 1, 14)]) + .build() + .execute_with(|| { + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 11 + 12 + 13 + 14 + 20 = 70 (top 4 + self bond) + assert_eq!(collator1_state.total_counted, 70); + // Top delegations are full, new highest delegation is made + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(7), 1, 15, 10, 10)); + assert_events_emitted!(Event::Delegation { + delegator: 7, + locked_amount: 15, + candidate: 1, + delegator_position: DelegatorAdded::AddedToTop { new_total: 74 }, + auto_compound: Percent::zero(), + }); + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 12 + 13 + 14 + 15 + 20 = 70 (top 4 + self bond) + assert_eq!(collator1_state.total_counted, 74); + // New delegation is added to the bottom + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(8), 1, 10, 10, 10)); + assert_events_emitted!(Event::Delegation { + delegator: 8, + locked_amount: 10, + candidate: 1, + delegator_position: DelegatorAdded::AddedToBottom, + auto_compound: Percent::zero(), + }); + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 12 + 13 + 14 + 15 + 20 = 70 (top 4 + self bond) + assert_eq!(collator1_state.total_counted, 74); + // 8 increases delegation to the top + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(8), 1, 3)); + assert_events_emitted!(Event::DelegationIncreased { delegator: 8, candidate: 1, amount: 3, in_top: true }); + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 13 + 13 + 14 + 15 + 20 = 75 (top 4 + self bond) + assert_eq!(collator1_state.total_counted, 75); + // 3 increases delegation but stays in bottom + assert_ok!(ParachainStaking::delegator_bond_more(RuntimeOrigin::signed(3), 1, 1)); + assert_events_emitted!(Event::DelegationIncreased { delegator: 3, candidate: 1, amount: 1, in_top: false }); + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 13 + 13 + 14 + 15 + 20 = 75 (top 4 + self bond) + assert_eq!(collator1_state.total_counted, 75); + // 6 decreases delegation but stays in top + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(6), 1, 2)); + assert_events_emitted!(Event::DelegationDecreaseScheduled { + delegator: 6, + candidate: 1, + amount_to_decrease: 2, + execute_round: 3, + }); + roll_to(30); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(6), 6, 1)); + assert_events_emitted!(Event::DelegationDecreased { delegator: 6, candidate: 1, amount: 2, in_top: true }); + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 12 + 13 + 13 + 15 + 20 = 73 (top 4 + self bond)ƒ + assert_eq!(collator1_state.total_counted, 73); + // 6 decreases delegation and is bumped to bottom + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(6), 1, 1)); + assert_events_emitted!(Event::DelegationDecreaseScheduled { + delegator: 6, + candidate: 1, + amount_to_decrease: 1, + execute_round: 9, + }); + roll_to(40); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(6), 6, 1)); + assert_events_emitted!(Event::DelegationDecreased { delegator: 6, candidate: 1, amount: 1, in_top: false }); + let collator1_state = ParachainStaking::candidate_info(1).unwrap(); + // 12 + 13 + 13 + 15 + 20 = 73 (top 4 + self bond) + assert_eq!(collator1_state.total_counted, 73); + }); +} + +#[test] +fn no_rewards_paid_until_after_reward_payment_delay() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20)]) + .build() + .execute_with(|| { + roll_to_round_begin(2); + // payouts for round 1 + set_author(1, 1, 1); + set_author(1, 2, 1); + set_author(1, 2, 1); + set_author(1, 3, 1); + set_author(1, 3, 1); + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 2, collator_account: 2, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 2, collator_account: 3, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 3, total_balance: 60 }, + ); + + roll_to_round_begin(3); + assert_events_eq!( + Event::CollatorChosen { round: 3, collator_account: 1, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 3, collator_account: 2, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 3, collator_account: 3, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 10, round: 3, selected_collators_number: 3, total_balance: 60 }, + ); + + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 3, rewards: 7 }); + + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 4 }); + + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 2, rewards: 7 }); + + // there should be no more payments in this round... + let num_blocks_rolled = roll_to_round_end(3); + assert_no_events!(); + assert_eq!(num_blocks_rolled, 1); + }); +} + +#[test] +fn deferred_payment_storage_items_are_cleaned_up() { + use crate::*; + + // this test sets up two collators, gives them points in round one, and focuses on the + // storage over the next several blocks to show that it is properly cleaned up + + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20)]) + .with_candidates(vec![(1, 20), (2, 20)]) + .build() + .execute_with(|| { + set_author(1, 1, 1); + set_author(1, 2, 1); + + // reflects genesis? + assert!(>::contains_key(1, 1)); + assert!(>::contains_key(1, 2)); + + roll_to_round_begin(2); + assert_events_eq!( + Event::CollatorChosen { round: 2, collator_account: 1, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 2, collator_account: 2, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 5, round: 2, selected_collators_number: 2, total_balance: 40 }, + ); + + // we should have AtStake snapshots as soon as we start a round... + assert!(>::contains_key(2, 1)); + assert!(>::contains_key(2, 2)); + // ...and it should persist until the round is fully paid out + assert!(>::contains_key(1, 1)); + assert!(>::contains_key(1, 2)); + + assert!( + !>::contains_key(1), + "DelayedPayouts shouldn't be populated until after RewardPaymentDelay" + ); + assert!(>::contains_key(1), "Points should be populated during current round"); + assert!(>::contains_key(1), "Staked should be populated when round changes"); + + assert!(!>::contains_key(2), "Points should not be populated until author noted"); + assert!(>::contains_key(2), "Staked should be populated when round changes"); + + // first payout occurs in round 3 + roll_to_round_begin(3); + assert_events_eq!( + Event::CollatorChosen { round: 3, collator_account: 1, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 3, collator_account: 2, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 10, round: 3, selected_collators_number: 2, total_balance: 40 }, + ); + + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 8 },); + + // payouts should exist for past rounds that haven't been paid out yet.. + assert!(>::contains_key(3, 1)); + assert!(>::contains_key(3, 2)); + assert!(>::contains_key(2, 1)); + assert!(>::contains_key(2, 2)); + + assert!( + >::contains_key(1), + "DelayedPayouts should be populated after RewardPaymentDelay" + ); + assert!(>::contains_key(1)); + assert!(!>::contains_key(1), "Staked should be cleaned up after round change"); + + assert!(!>::contains_key(2)); + assert!(!>::contains_key(2), "We never rewarded points for round 2"); + assert!(>::contains_key(2)); + + assert!(!>::contains_key(3)); + assert!(!>::contains_key(3), "We never awarded points for round 3"); + assert!(>::contains_key(3)); + + // collator 1 has been paid in this last block and associated storage cleaned up + assert!(!>::contains_key(1, 1)); + assert!(!>::contains_key(1, 1)); + + // but collator 2 hasn't been paid + assert!(>::contains_key(1, 2)); + assert!(>::contains_key(1, 2)); + + // second payout occurs in next block + roll_blocks(1); + assert_events_eq!(Event::Rewarded { account: 2, rewards: 8 },); + + roll_to_round_begin(4); + assert_events_eq!( + Event::CollatorChosen { round: 4, collator_account: 1, total_exposed_amount: 20 }, + Event::CollatorChosen { round: 4, collator_account: 2, total_exposed_amount: 20 }, + Event::NewRound { starting_block: 15, round: 4, selected_collators_number: 2, total_balance: 40 }, + ); + + // collators have both been paid and storage fully cleaned up for round 1 + assert!(!>::contains_key(1, 2)); + assert!(!>::contains_key(1, 2)); + assert!(!>::contains_key(1)); + assert!(!>::contains_key(1)); // points should be cleaned up + assert!(!>::contains_key(1)); + + roll_to_round_end(4); + + // no more events expected + assert_no_events!(); + }); +} + +#[test] +fn deferred_payment_and_at_stake_storage_items_cleaned_up_for_candidates_not_producing_blocks() { + use crate::*; + + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 20), (2, 20), (3, 20)]) + .build() + .execute_with(|| { + // candidate 3 will not produce blocks + set_author(1, 1, 1); + set_author(1, 2, 1); + + // reflects genesis? + assert!(>::contains_key(1, 1)); + assert!(>::contains_key(1, 2)); + + roll_to_round_begin(2); + assert!(>::contains_key(1, 1)); + assert!(>::contains_key(1, 2)); + assert!(>::contains_key(1, 3)); + assert!(>::contains_key(1, 1)); + assert!(>::contains_key(1, 2)); + assert!(!>::contains_key(1, 3)); + assert!(>::contains_key(1)); + assert!(>::contains_key(1)); + roll_to_round_begin(3); + assert!(>::contains_key(1)); + + // all storage items must be cleaned up + roll_to_round_begin(4); + assert!(!>::contains_key(1, 1)); + assert!(!>::contains_key(1, 2)); + assert!(!>::contains_key(1, 3)); + assert!(!>::contains_key(1, 1)); + assert!(!>::contains_key(1, 2)); + assert!(!>::contains_key(1, 3)); + assert!(!>::contains_key(1)); + assert!(!>::contains_key(1)); + assert!(!>::contains_key(1)); + }); +} + +#[test] +fn deferred_payment_steady_state_event_flow() { + use frame_support::traits::{Currency, ExistenceRequirement, WithdrawReasons}; + + // this test "flows" through a number of rounds, asserting that certain things do/don't happen + // once the staking pallet is in a "steady state" (specifically, once we are past the first few + // rounds to clear RewardPaymentDelay) + + ExtBuilder::default() + .with_balances(vec![ + // collators + (1, 200), + (2, 200), + (3, 200), + (4, 200), + // delegators + (11, 200), + (22, 200), + (33, 200), + (44, 200), + // burn account, see `reset_issuance()` + (111, 1000), + ]) + .with_candidates(vec![(1, 200), (2, 200), (3, 200), (4, 200)]) + .with_delegations(vec![ + // delegator 11 delegates 100 to 1 and 2 + (11, 1, 100), + (11, 2, 100), + // delegator 22 delegates 100 to 2 and 3 + (22, 2, 100), + (22, 3, 100), + // delegator 33 delegates 100 to 3 and 4 + (33, 3, 100), + (33, 4, 100), + // delegator 44 delegates 100 to 4 and 1 + (44, 4, 100), + (44, 1, 100), + ]) + .build() + .execute_with(|| { + // convenience to set the round points consistently + let set_round_points = |round: BlockNumber| { + set_author(round, 1, 1); + set_author(round, 2, 1); + set_author(round, 3, 1); + set_author(round, 4, 1); + }; + + // grab initial issuance -- we will reset it before round issuance is calculated so that + // it is consistent every round + let initial_issuance = Balances::total_issuance(); + let reset_issuance = || { + let new_issuance = Balances::total_issuance(); + let diff = new_issuance - initial_issuance; + assert_ok!(Balances::burn(Some(111).into())); + let burned_imbalance = PositiveImbalance::::new(diff); + + Balances::settle(&111, burned_imbalance, WithdrawReasons::FEE, ExistenceRequirement::AllowDeath) + .expect("Account can absorb burn"); + }; + + // fn to roll through the first RewardPaymentDelay rounds. returns new round index + let roll_through_initial_rounds = |mut round: BlockNumber| -> BlockNumber { + while round < crate::mock::RewardPaymentDelay::get() + 1 { + set_round_points(round); + + roll_to_round_end(round); + round += 1; + } + + reset_issuance(); + + round + }; + + // roll through a "steady state" round and make all of our assertions + // returns new round index + let roll_through_steady_state_round = |round: BlockNumber| -> BlockNumber { + let num_rounds_rolled = roll_to_round_begin(round); + assert!(num_rounds_rolled <= 1, "expected to be at round begin already"); + + assert_events_eq!( + Event::CollatorChosen { round: round as u32, collator_account: 1, total_exposed_amount: 400 }, + Event::CollatorChosen { round: round as u32, collator_account: 2, total_exposed_amount: 400 }, + Event::CollatorChosen { round: round as u32, collator_account: 3, total_exposed_amount: 400 }, + Event::CollatorChosen { round: round as u32, collator_account: 4, total_exposed_amount: 400 }, + Event::NewRound { + starting_block: (round as u32 - 1) * 5, + round: round as u32, + selected_collators_number: 4, + total_balance: 1600, + }, + ); + + set_round_points(round); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 3, rewards: 21 }, + Event::Rewarded { account: 22, rewards: 7 }, + Event::Rewarded { account: 33, rewards: 7 }, + ); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 4, rewards: 21 }, + Event::Rewarded { account: 33, rewards: 7 }, + Event::Rewarded { account: 44, rewards: 7 }, + ); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 21 }, + Event::Rewarded { account: 11, rewards: 7 }, + Event::Rewarded { account: 44, rewards: 7 }, + ); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 2, rewards: 21 }, + Event::Rewarded { account: 11, rewards: 7 }, + Event::Rewarded { account: 22, rewards: 7 }, + ); + + roll_blocks(1); + // Since we defer first deferred staking payout, this test have the maximum amout of + // supported collators. This eman that the next round is trigerred one block after + // the last reward. + //assert_no_events!(); + + let num_rounds_rolled = roll_to_round_end(round); + assert_eq!(num_rounds_rolled, 0, "expected to be at round end already"); + + reset_issuance(); + + round + 1 + }; + + let mut round = 1; + round = roll_through_initial_rounds(round); // we should be at RewardPaymentDelay + for _ in 1..2 { + round = roll_through_steady_state_round(round); + } + }); +} + +#[test] +fn delegation_kicked_from_bottom_removes_pending_request() { + ExtBuilder::default() + .with_balances(vec![ + (1, 30), + (2, 29), + (3, 20), + (4, 20), + (5, 20), + (6, 20), + (7, 20), + (8, 20), + (9, 20), + (10, 20), + (11, 30), + ]) + .with_candidates(vec![(1, 30), (11, 30)]) + .with_delegations(vec![ + (2, 1, 19), + (2, 11, 10), // second delegation so not left after first is kicked + (3, 1, 20), + (4, 1, 20), + (5, 1, 20), + (6, 1, 20), + (7, 1, 20), + (8, 1, 20), + (9, 1, 20), + ]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + // 10 delegates to full 1 => kicks lowest delegation (2, 19) + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(10), 1, 20, 8, 0)); + // check the event + assert_events_emitted!(Event::DelegationKicked { delegator: 2, candidate: 1, unstaked_amount: 19 }); + // ensure request DNE + assert!(!ParachainStaking::delegation_scheduled_requests(&1).iter().any(|x| x.delegator == 2)); + }); +} + +#[test] +fn no_selected_candidates_defaults_to_last_round_collators() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 30), (3, 30), (4, 30), (5, 30)]) + .with_candidates(vec![(1, 30), (2, 30), (3, 30), (4, 30), (5, 30)]) + .build() + .execute_with(|| { + roll_to_round_begin(1); + // schedule to leave + for i in 1..6 { + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(i), 5)); + } + let old_round = ParachainStaking::round().current; + let old_selected_candidates = ParachainStaking::selected_candidates(); + let mut old_at_stake_snapshots = Vec::new(); + for account in old_selected_candidates.clone() { + old_at_stake_snapshots.push(>::get(old_round, account)); + } + roll_to_round_begin(3); + // execute leave + for i in 1..6 { + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(i), i, 0,)); + } + // next round + roll_to_round_begin(4); + let new_round = ParachainStaking::round().current; + // check AtStake matches previous + let new_selected_candidates = ParachainStaking::selected_candidates(); + assert_eq!(old_selected_candidates, new_selected_candidates); + let mut index = 0usize; + for account in new_selected_candidates { + assert_eq!(old_at_stake_snapshots[index], >::get(new_round, account)); + index += 1usize; + } + }); +} + +#[test] +fn test_delegator_scheduled_for_revoke_is_rewarded_for_previous_rounds_but_not_for_future() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + // preset rewards for rounds 1, 2 and 3 + (1..=3).for_each(|round| set_author(round, 1, 1)); + + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_events_eq!(Event::DelegationRevocationScheduled { + round: 1, + delegator: 2, + candidate: 1, + scheduled_exit: 3, + }); + let collator = ParachainStaking::candidate_info(1).expect("candidate must exist"); + assert_eq!(1, collator.delegation_count, "collator's delegator count was reduced unexpectedly"); + assert_eq!(30, collator.total_counted, "collator's total was reduced unexpectedly"); + + roll_to_round_begin(3); + assert_events_emitted_match!(Event::NewRound { round: 3, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 15 }, Event::Rewarded { account: 2, rewards: 5 },); + + roll_to_round_begin(4); + assert_events_emitted_match!(Event::NewRound { round: 4, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 20 },); + let collator_snapshot = ParachainStaking::at_stake(ParachainStaking::round().current, 1); + assert_eq!( + 1, + collator_snapshot.delegations.len(), + "collator snapshot's delegator count was reduced unexpectedly" + ); + assert_eq!(20, collator_snapshot.total, "collator snapshot's total was reduced unexpectedly",); + }); +} + +#[test] +fn test_delegator_scheduled_for_revoke_is_rewarded_when_request_cancelled() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + // preset rewards for rounds 2, 3 and 4 + (2..=4).for_each(|round| set_author(round, 1, 1)); + + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_events_eq!(Event::DelegationRevocationScheduled { + round: 1, + delegator: 2, + candidate: 1, + scheduled_exit: 3, + }); + let collator = ParachainStaking::candidate_info(1).expect("candidate must exist"); + assert_eq!(1, collator.delegation_count, "collator's delegator count was reduced unexpectedly"); + assert_eq!(30, collator.total_counted, "collator's total was reduced unexpectedly"); + + roll_to_round_begin(2); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + + roll_to_round_begin(4); + assert_events_emitted_match!(Event::NewRound { round: 4, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 20 },); + let collator_snapshot = ParachainStaking::at_stake(ParachainStaking::round().current, 1); + assert_eq!( + 1, + collator_snapshot.delegations.len(), + "collator snapshot's delegator count was reduced unexpectedly" + ); + assert_eq!(30, collator_snapshot.total, "collator snapshot's total was reduced unexpectedly",); + + roll_to_round_begin(5); + assert_events_emitted_match!(Event::NewRound { round: 5, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 15 }, Event::Rewarded { account: 2, rewards: 5 },); + }); +} + +#[test] +fn test_delegator_scheduled_for_bond_decrease_is_rewarded_for_previous_rounds_but_less_for_future() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 20), (2, 3, 10)]) + .build() + .execute_with(|| { + // preset rewards for rounds 1, 2 and 3 + (1..=3).for_each(|round| set_author(round, 1, 1)); + + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 10,)); + assert_events_eq!(Event::DelegationDecreaseScheduled { + execute_round: 3, + delegator: 2, + candidate: 1, + amount_to_decrease: 10, + }); + let collator = ParachainStaking::candidate_info(1).expect("candidate must exist"); + assert_eq!(1, collator.delegation_count, "collator's delegator count was reduced unexpectedly"); + assert_eq!(40, collator.total_counted, "collator's total was reduced unexpectedly"); + + roll_to_round_begin(3); + assert_events_emitted_match!(Event::NewRound { round: 3, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 12 }, Event::Rewarded { account: 2, rewards: 8 },); + + roll_to_round_begin(4); + assert_events_emitted_match!(Event::NewRound { round: 4, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 15 }, Event::Rewarded { account: 2, rewards: 5 },); + let collator_snapshot = ParachainStaking::at_stake(ParachainStaking::round().current, 1); + assert_eq!( + 1, + collator_snapshot.delegations.len(), + "collator snapshot's delegator count was reduced unexpectedly" + ); + assert_eq!(30, collator_snapshot.total, "collator snapshot's total was reduced unexpectedly",); + }); +} + +#[test] +fn test_delegator_scheduled_for_bond_decrease_is_rewarded_when_request_cancelled() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 20), (2, 3, 10)]) + .build() + .execute_with(|| { + // preset rewards for rounds 2, 3 and 4 + (2..=4).for_each(|round| set_author(round, 1, 1)); + + assert_ok!(ParachainStaking::schedule_delegator_bond_less(RuntimeOrigin::signed(2), 1, 10,)); + assert_events_eq!(Event::DelegationDecreaseScheduled { + execute_round: 3, + delegator: 2, + candidate: 1, + amount_to_decrease: 10, + }); + let collator = ParachainStaking::candidate_info(1).expect("candidate must exist"); + assert_eq!(1, collator.delegation_count, "collator's delegator count was reduced unexpectedly"); + assert_eq!(40, collator.total_counted, "collator's total was reduced unexpectedly"); + + roll_to_round_begin(2); + assert_ok!(ParachainStaking::cancel_delegation_request(RuntimeOrigin::signed(2), 1)); + + roll_to_round_begin(4); + assert_events_emitted_match!(Event::NewRound { round: 4, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 15 }, Event::Rewarded { account: 2, rewards: 5 },); + let collator_snapshot = ParachainStaking::at_stake(ParachainStaking::round().current, 1); + assert_eq!( + 1, + collator_snapshot.delegations.len(), + "collator snapshot's delegator count was reduced unexpectedly" + ); + assert_eq!(40, collator_snapshot.total, "collator snapshot's total was reduced unexpectedly",); + + roll_to_round_begin(5); + assert_events_emitted_match!(Event::NewRound { round: 5, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 12 }, Event::Rewarded { account: 2, rewards: 8 },); + }); +} + +#[test] +fn test_delegator_scheduled_for_leave_is_rewarded_for_previous_rounds_but_not_for_future() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + // preset rewards for rounds 1, 2 and 3 + (1..=3).for_each(|round| set_author(round, 1, 1)); + + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2),)); + assert_events_eq!(Event::DelegatorExitScheduled { round: 1, delegator: 2, scheduled_exit: 3 }); + let collator = ParachainStaking::candidate_info(1).expect("candidate must exist"); + assert_eq!(1, collator.delegation_count, "collator's delegator count was reduced unexpectedly"); + assert_eq!(30, collator.total_counted, "collator's total was reduced unexpectedly"); + + roll_to_round_begin(3); + assert_events_emitted_match!(Event::NewRound { round: 3, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 15 }, Event::Rewarded { account: 2, rewards: 5 },); + + roll_to_round_begin(4); + assert_events_emitted_match!(Event::NewRound { round: 4, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 20 },); + let collator_snapshot = ParachainStaking::at_stake(ParachainStaking::round().current, 1); + assert_eq!( + 1, + collator_snapshot.delegations.len(), + "collator snapshot's delegator count was reduced unexpectedly" + ); + assert_eq!(20, collator_snapshot.total, "collator snapshot's total was reduced unexpectedly",); + }); +} + +#[test] +fn test_delegator_scheduled_for_leave_is_rewarded_when_request_cancelled() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + // preset rewards for rounds 2, 3 and 4 + (2..=4).for_each(|round| set_author(round, 1, 1)); + + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_events_eq!(Event::DelegatorExitScheduled { round: 1, delegator: 2, scheduled_exit: 3 }); + let collator = ParachainStaking::candidate_info(1).expect("candidate must exist"); + assert_eq!(1, collator.delegation_count, "collator's delegator count was reduced unexpectedly"); + assert_eq!(30, collator.total_counted, "collator's total was reduced unexpectedly"); + + roll_to_round_begin(2); + assert_ok!(ParachainStaking::cancel_leave_delegators(RuntimeOrigin::signed(2))); + + roll_to_round_begin(4); + assert_events_emitted_match!(Event::NewRound { round: 4, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 20 },); + let collator_snapshot = ParachainStaking::at_stake(ParachainStaking::round().current, 1); + assert_eq!( + 1, + collator_snapshot.delegations.len(), + "collator snapshot's delegator count was reduced unexpectedly" + ); + assert_eq!(30, collator_snapshot.total, "collator snapshot's total was reduced unexpectedly",); + + roll_to_round_begin(5); + assert_events_emitted_match!(Event::NewRound { round: 5, .. }); + roll_blocks(3); + assert_events_eq!(Event::Rewarded { account: 1, rewards: 15 }, Event::Rewarded { account: 2, rewards: 5 },); + }); +} + +#[test] +fn test_delegation_request_exists_returns_false_when_nothing_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert!(!ParachainStaking::delegation_request_exists(&1, &2)); + }); +} + +#[test] +fn test_delegation_request_exists_returns_true_when_decrease_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::insert( + 1, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Decrease(5) }], + ); + assert!(ParachainStaking::delegation_request_exists(&1, &2)); + }); +} + +#[test] +fn test_delegation_request_exists_returns_true_when_revoke_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::insert( + 1, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Revoke(5) }], + ); + assert!(ParachainStaking::delegation_request_exists(&1, &2)); + }); +} + +#[test] +fn test_delegation_request_revoke_exists_returns_false_when_nothing_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert!(!ParachainStaking::delegation_request_revoke_exists(&1, &2)); + }); +} + +#[test] +fn test_delegation_request_revoke_exists_returns_false_when_decrease_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::insert( + 1, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Decrease(5) }], + ); + assert!(!ParachainStaking::delegation_request_revoke_exists(&1, &2)); + }); +} + +#[test] +fn test_delegation_request_revoke_exists_returns_true_when_revoke_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::insert( + 1, + vec![ScheduledRequest { delegator: 2, when_executable: 3, action: DelegationAction::Revoke(5) }], + ); + assert!(ParachainStaking::delegation_request_revoke_exists(&1, &2)); + }); +} + +#[test] +fn test_hotfix_remove_delegation_requests_exited_candidates_cleans_up() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + // invalid state + >::insert(2, Vec::>::new()); + >::insert(3, Vec::>::new()); + assert_ok!(ParachainStaking::hotfix_remove_delegation_requests_exited_candidates( + RuntimeOrigin::signed(1), + vec![2, 3, 4] // 4 does not exist, but is OK for idempotency + )); + + assert!(!>::contains_key(2)); + assert!(!>::contains_key(3)); + }); +} + +#[test] +fn test_hotfix_remove_delegation_requests_exited_candidates_cleans_up_only_specified_keys() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + // invalid state + >::insert(2, Vec::>::new()); + >::insert(3, Vec::>::new()); + assert_ok!(ParachainStaking::hotfix_remove_delegation_requests_exited_candidates( + RuntimeOrigin::signed(1), + vec![2] + )); + + assert!(!>::contains_key(2)); + assert!(>::contains_key(3)); + }); +} + +#[test] +fn test_hotfix_remove_delegation_requests_exited_candidates_errors_when_requests_not_empty() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + // invalid state + >::insert(2, Vec::>::new()); + >::insert( + 3, + vec![ScheduledRequest { delegator: 10, when_executable: 1, action: DelegationAction::Revoke(10) }], + ); + + assert_noop!( + ParachainStaking::hotfix_remove_delegation_requests_exited_candidates(RuntimeOrigin::signed(1), vec![2, 3]), + >::CandidateNotLeaving, + ); + }); +} + +#[test] +fn test_hotfix_remove_delegation_requests_exited_candidates_errors_when_candidate_not_exited() { + ExtBuilder::default().with_balances(vec![(1, 20)]).with_candidates(vec![(1, 20)]).build().execute_with(|| { + // invalid state + >::insert(1, Vec::>::new()); + assert_noop!( + ParachainStaking::hotfix_remove_delegation_requests_exited_candidates(RuntimeOrigin::signed(1), vec![1]), + >::CandidateNotLeaving, + ); + }); +} + +#[test] +fn revoke_last_removes_lock() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 100), (3, 100)]) + .with_candidates(vec![(1, 25), (2, 25)]) + .with_delegations(vec![(3, 1, 30), (3, 2, 25)]) + .build() + .execute_with(|| { + assert_eq!(crate::mock::Balances::reserved_balance(&3), 55); + + // schedule and remove one... + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(3), 1)); + roll_to_round_begin(3); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(3), 3, 1)); + assert_eq!(crate::mock::Balances::reserved_balance(&3), 25); + + // schedule and remove the other... + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(3), 2)); + roll_to_round_begin(5); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(3), 3, 2)); + assert_eq!(crate::mock::Balances::reserved_balance(&3), 0); + }); +} + +#[allow(deprecated)] +#[test] +fn test_delegator_with_deprecated_status_leaving_can_schedule_leave_delegators_as_fix() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::mutate(2, |value| { + value.as_mut().map(|state| { + state.status = DelegatorStatus::Leaving(2); + }) + }); + let state = >::get(2); + assert!(matches!(state.unwrap().status, DelegatorStatus::Leaving(_))); + + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert!(>::get(1) + .iter() + .any(|r| r.delegator == 2 && matches!(r.action, DelegationAction::Revoke(_)))); + assert_events_eq!(Event::DelegatorExitScheduled { round: 1, delegator: 2, scheduled_exit: 3 }); + + let state = >::get(2); + assert!(matches!(state.unwrap().status, DelegatorStatus::Active)); + }); +} + +#[allow(deprecated)] +#[test] +fn test_delegator_with_deprecated_status_leaving_can_cancel_leave_delegators_as_fix() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::mutate(2, |value| { + value.as_mut().map(|state| { + state.status = DelegatorStatus::Leaving(2); + }) + }); + let state = >::get(2); + assert!(matches!(state.unwrap().status, DelegatorStatus::Leaving(_))); + + assert_ok!(ParachainStaking::cancel_leave_delegators(RuntimeOrigin::signed(2))); + assert_events_eq!(Event::DelegatorExitCancelled { delegator: 2 }); + + let state = >::get(2); + assert!(matches!(state.unwrap().status, DelegatorStatus::Active)); + }); +} + +#[allow(deprecated)] +#[test] +fn test_delegator_with_deprecated_status_leaving_can_execute_leave_delegators_as_fix() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::mutate(2, |value| { + value.as_mut().map(|state| { + state.status = DelegatorStatus::Leaving(2); + }) + }); + let state = >::get(2); + assert!(matches!(state.unwrap().status, DelegatorStatus::Leaving(_))); + + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1)); + assert_events_emitted!(Event::DelegatorLeft { delegator: 2, unstaked_amount: 10 }); + + let state = >::get(2); + assert!(state.is_none()); + }); +} + +#[allow(deprecated)] +#[test] +fn test_delegator_with_deprecated_status_leaving_cannot_execute_leave_delegators_early_no_fix() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 40)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::mutate(2, |value| { + value.as_mut().map(|state| { + state.status = DelegatorStatus::Leaving(2); + }) + }); + let state = >::get(2); + assert!(matches!(state.unwrap().status, DelegatorStatus::Leaving(_))); + + assert_noop!( + ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 1), + Error::::DelegatorCannotLeaveYet + ); + }); +} + +#[test] +fn test_set_auto_compound_fails_if_invalid_delegation_hint() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + let candidate_auto_compounding_delegation_count_hint = 0; + let delegation_hint = 0; // is however, 1 + + assert_noop!( + ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + candidate_auto_compounding_delegation_count_hint, + delegation_hint, + ), + >::TooLowDelegationCountToAutoCompound, + ); + }); +} + +#[test] +fn test_set_auto_compound_fails_if_invalid_candidate_auto_compounding_hint() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::new( + vec![AutoCompoundConfig { delegator: 2, value: Percent::from_percent(10) }] + .try_into() + .expect("must succeed"), + ) + .set_storage(&1); + let candidate_auto_compounding_delegation_count_hint = 0; // is however, 1 + let delegation_hint = 1; + + assert_noop!( + ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + candidate_auto_compounding_delegation_count_hint, + delegation_hint, + ), + >::TooLowCandidateAutoCompoundingDelegationCountToAutoCompound, + ); + }); +} + +#[test] +fn test_set_auto_compound_inserts_if_not_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 1, + )); + assert_events_emitted!(Event::AutoCompoundSet { + candidate: 1, + delegator: 2, + value: Percent::from_percent(50), + }); + assert_eq!( + vec![AutoCompoundConfig { delegator: 2, value: Percent::from_percent(50) }], + ParachainStaking::auto_compounding_delegations(&1).into_inner(), + ); + }); +} + +#[test] +fn test_set_auto_compound_updates_if_existing() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::new( + vec![AutoCompoundConfig { delegator: 2, value: Percent::from_percent(10) }] + .try_into() + .expect("must succeed"), + ) + .set_storage(&1); + + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 1, + 1, + )); + assert_events_emitted!(Event::AutoCompoundSet { + candidate: 1, + delegator: 2, + value: Percent::from_percent(50), + }); + assert_eq!( + vec![AutoCompoundConfig { delegator: 2, value: Percent::from_percent(50) }], + ParachainStaking::auto_compounding_delegations(&1).into_inner(), + ); + }); +} + +#[test] +fn test_set_auto_compound_removes_if_auto_compound_zero_percent() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + >::new( + vec![AutoCompoundConfig { delegator: 2, value: Percent::from_percent(10) }] + .try_into() + .expect("must succeed"), + ) + .set_storage(&1); + + assert_ok!(ParachainStaking::set_auto_compound(RuntimeOrigin::signed(2), 1, Percent::zero(), 1, 1,)); + assert_events_emitted!(Event::AutoCompoundSet { candidate: 1, delegator: 2, value: Percent::zero() }); + assert_eq!(0, ParachainStaking::auto_compounding_delegations(&1).len(),); + }); +} + +#[test] +fn test_execute_revoke_delegation_removes_auto_compounding_from_state_for_delegation_revoke() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 30), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 2, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 3, + Percent::from_percent(50), + 0, + 2, + )); + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to(10); + assert_ok!(ParachainStaking::execute_delegation_request(RuntimeOrigin::signed(2), 2, 1)); + assert!( + !ParachainStaking::auto_compounding_delegations(&1).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + assert!( + ParachainStaking::auto_compounding_delegations(&3).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was erroneously removed" + ); + }); +} + +#[test] +fn test_execute_leave_delegators_removes_auto_compounding_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 2, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 3, + Percent::from_percent(50), + 0, + 2, + )); + + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 2,)); + + assert!( + !ParachainStaking::auto_compounding_delegations(&1).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + assert!( + !ParachainStaking::auto_compounding_delegations(&3).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + }); +} + +#[allow(deprecated)] +#[test] +fn test_execute_leave_delegators_with_deprecated_status_leaving_removes_auto_compounding_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 2, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 3, + Percent::from_percent(50), + 0, + 2, + )); + + >::mutate(2, |value| { + value.as_mut().map(|state| { + state.status = DelegatorStatus::Leaving(2); + }) + }); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_delegators(RuntimeOrigin::signed(2), 2, 2,)); + + assert!( + !ParachainStaking::auto_compounding_delegations(&1).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + assert!( + !ParachainStaking::auto_compounding_delegations(&3).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + }); +} + +#[test] +fn test_execute_leave_candidates_removes_auto_compounding_state() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 30), (3, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 2, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 3, + Percent::from_percent(50), + 0, + 2, + )); + + assert_ok!(ParachainStaking::schedule_leave_candidates(RuntimeOrigin::signed(1), 2)); + roll_to(10); + assert_ok!(ParachainStaking::execute_leave_candidates(RuntimeOrigin::signed(1), 1, 1,)); + + assert!( + !ParachainStaking::auto_compounding_delegations(&1).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + assert!( + ParachainStaking::auto_compounding_delegations(&3).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was erroneously removed" + ); + }); +} + +#[test] +fn test_delegation_kicked_from_bottom_delegation_removes_auto_compounding_state() { + ExtBuilder::default() + .with_balances(vec![ + (1, 30), + (2, 29), + (3, 20), + (4, 20), + (5, 20), + (6, 20), + (7, 20), + (8, 20), + (9, 20), + (10, 20), + (11, 30), + ]) + .with_candidates(vec![(1, 30), (11, 30)]) + .with_delegations(vec![ + (2, 11, 10), // extra delegation to avoid leaving the delegator set + (2, 1, 19), + (3, 1, 20), + (4, 1, 20), + (5, 1, 20), + (6, 1, 20), + (7, 1, 20), + (8, 1, 20), + (9, 1, 20), + ]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 2, + )); + + // kicks lowest delegation (2, 19) + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(10), 1, 20, 8, 0)); + + assert!( + !ParachainStaking::auto_compounding_delegations(&1).iter().any(|x| x.delegator == 2), + "delegation auto-compound config was not removed" + ); + }); +} + +#[test] +fn test_rewards_do_not_auto_compound_on_payment_if_delegation_scheduled_revoke_exists() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 200), (3, 200)]) + .with_candidates(vec![(1, 100)]) + .with_delegations(vec![(2, 1, 200), (3, 1, 200)]) + .build() + .execute_with(|| { + (2..=5).for_each(|round| set_author(round, 1, 1)); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(50), + 0, + 1, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(3), + 1, + Percent::from_percent(50), + 1, + 1, + )); + roll_to_round_begin(3); + + // schedule revoke for delegator 2; no rewards should be compounded + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + roll_to_round_begin(4); + + assert_events_eq!( + Event::CollatorChosen { round: 4, collator_account: 1, total_exposed_amount: 500 }, + Event::NewRound { starting_block: 15, round: 4, selected_collators_number: 1, total_balance: 500 }, + ); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 14 }, + // no compound since revoke request exists + Event::Rewarded { account: 2, rewards: 13 }, + // 50% + Event::Rewarded { account: 3, rewards: 13 }, + Event::Compounded { candidate: 1, delegator: 3, amount: 7 }, + ); + }); +} + +#[test] +fn test_rewards_auto_compound_on_payment_as_per_auto_compound_config() { + ExtBuilder::default() + .with_balances(vec![(1, 100), (2, 200), (3, 200), (4, 200), (5, 200)]) + .with_candidates(vec![(1, 100)]) + .with_delegations(vec![(2, 1, 200), (3, 1, 200), (4, 1, 200), (5, 1, 200)]) + .build() + .execute_with(|| { + (2..=6).for_each(|round| set_author(round, 1, 1)); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(2), + 1, + Percent::from_percent(0), + 0, + 1, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(3), + 1, + Percent::from_percent(50), + 1, + 1, + )); + assert_ok!(ParachainStaking::set_auto_compound( + RuntimeOrigin::signed(4), + 1, + Percent::from_percent(100), + 2, + 1, + )); + roll_to_round_begin(4); + + assert_events_eq!( + Event::CollatorChosen { round: 4, collator_account: 1, total_exposed_amount: 900 }, + Event::NewRound { starting_block: 15, round: 4, selected_collators_number: 1, total_balance: 900 }, + ); + + roll_blocks(1); + assert_events_eq!( + Event::Rewarded { account: 1, rewards: 17 }, + // 0% + Event::Rewarded { account: 2, rewards: 11 }, + // 50% + Event::Rewarded { account: 3, rewards: 11 }, + Event::Compounded { candidate: 1, delegator: 3, amount: 6 }, + // 100% + Event::Rewarded { account: 4, rewards: 11 }, + Event::Compounded { candidate: 1, delegator: 4, amount: 11 }, + // no-config + Event::Rewarded { account: 5, rewards: 11 }, + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_fails_if_invalid_delegation_hint() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25), (3, 30)]) + .with_candidates(vec![(1, 30), (3, 30)]) + .with_delegations(vec![(2, 3, 10)]) + .build() + .execute_with(|| { + let candidate_delegation_count_hint = 0; + let candidate_auto_compounding_delegation_count_hint = 0; + let delegation_hint = 0; // is however, 1 + + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + candidate_delegation_count_hint, + candidate_auto_compounding_delegation_count_hint, + delegation_hint, + ), + >::TooLowDelegationCountToDelegate, + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_fails_if_invalid_candidate_delegation_count_hint() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25), (3, 30)]) + .with_candidates(vec![(1, 30)]) + .with_delegations(vec![(3, 1, 10)]) + .build() + .execute_with(|| { + let candidate_delegation_count_hint = 0; // is however, 1 + let candidate_auto_compounding_delegation_count_hint = 0; + let delegation_hint = 0; + + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + candidate_delegation_count_hint, + candidate_auto_compounding_delegation_count_hint, + delegation_hint, + ), + >::TooLowCandidateDelegationCountToDelegate, + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_fails_if_invalid_candidate_auto_compounding_delegations_hint() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 25), (3, 30)]) + .with_candidates(vec![(1, 30)]) + .with_auto_compounding_delegations(vec![(3, 1, 10, Percent::from_percent(10))]) + .build() + .execute_with(|| { + let candidate_delegation_count_hint = 1; + let candidate_auto_compounding_delegation_count_hint = 0; // is however, 1 + let delegation_hint = 0; + + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + candidate_delegation_count_hint, + candidate_auto_compounding_delegation_count_hint, + delegation_hint, + ), + >::TooLowCandidateAutoCompoundingDelegationCountToDelegate, + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_sets_auto_compound_config() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 25)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + 0, + 0, + 0, + )); + assert_events_emitted!(Event::Delegation { + delegator: 2, + locked_amount: 10, + candidate: 1, + delegator_position: DelegatorAdded::AddedToTop { new_total: 40 }, + auto_compound: Percent::from_percent(50), + }); + assert_eq!( + vec![AutoCompoundConfig { delegator: 2, value: Percent::from_percent(50) }], + ParachainStaking::auto_compounding_delegations(&1).into_inner(), + ); + }, + ); +} + +#[test] +fn test_delegate_with_auto_compound_skips_storage_but_emits_event_for_zero_auto_compound() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::zero(), + 0, + 0, + 0, + )); + assert_eq!(0, ParachainStaking::auto_compounding_delegations(&1).len(),); + assert_events_eq!(Event::Delegation { + delegator: 2, + locked_amount: 10, + candidate: 1, + delegator_position: DelegatorAdded::AddedToTop { new_total: 40 }, + auto_compound: Percent::zero(), + }); + }, + ); +} + +#[test] +fn test_delegate_with_auto_compound_reserves_balance() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 10); + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + 0, + 0, + 0, + )); + assert_eq!(ParachainStaking::get_delegator_stakable_free_balance(&2), 0); + }, + ); +} + +#[test] +fn test_delegate_with_auto_compound_updates_delegator_state() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + assert!(ParachainStaking::delegator_state(2).is_none()); + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + 0, + 0, + 0 + )); + let delegator_state = ParachainStaking::delegator_state(2).expect("just delegated => exists"); + assert_eq!(delegator_state.total(), 10); + assert_eq!(delegator_state.delegations.0[0].owner, 1); + assert_eq!(delegator_state.delegations.0[0].amount, 10); + }, + ); +} + +#[test] +fn test_delegate_with_auto_compound_updates_collator_state() { + ExtBuilder::default().with_balances(vec![(1, 30), (2, 10)]).with_candidates(vec![(1, 30)]).build().execute_with( + || { + let candidate_state = ParachainStaking::candidate_info(1).expect("registered in genesis"); + assert_eq!(candidate_state.total_counted, 30); + let top_delegations = ParachainStaking::top_delegations(1).expect("registered in genesis"); + assert!(top_delegations.delegations.is_empty()); + assert!(top_delegations.total.is_zero()); + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + 0, + 0, + 0 + )); + let candidate_state = ParachainStaking::candidate_info(1).expect("just delegated => exists"); + assert_eq!(candidate_state.total_counted, 40); + let top_delegations = ParachainStaking::top_delegations(1).expect("just delegated => exists"); + assert_eq!(top_delegations.delegations[0].owner, 2); + assert_eq!(top_delegations.delegations[0].amount, 10); + assert_eq!(top_delegations.total, 10); + }, + ); +} + +#[test] +fn test_delegate_with_auto_compound_can_delegate_immediately_after_other_join_candidates() { + ExtBuilder::default().with_balances(vec![(1, 20), (2, 20)]).build().execute_with(|| { + assert_ok!(ParachainStaking::join_candidates(RuntimeOrigin::signed(1), 20, 0)); + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 20, + Percent::from_percent(50), + 0, + 0, + 0 + )); + }); +} + +#[test] +fn test_delegate_with_auto_compound_can_delegate_to_other_if_revoking() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 30), (3, 20), (4, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_revoke_delegation(RuntimeOrigin::signed(2), 1)); + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 4, + 10, + Percent::from_percent(50), + 0, + 0, + 2 + )); + }); +} + +#[test] +fn test_delegate_with_auto_compound_cannot_delegate_if_less_than_or_equal_lowest_bottom() { + ExtBuilder::default() + .with_balances(vec![ + (1, 20), + (2, 10), + (3, 10), + (4, 10), + (5, 10), + (6, 10), + (7, 10), + (8, 10), + (9, 10), + (10, 10), + (11, 10), + ]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![ + (2, 1, 10), + (3, 1, 10), + (4, 1, 10), + (5, 1, 10), + (6, 1, 10), + (8, 1, 10), + (9, 1, 10), + (10, 1, 10), + ]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(11), + 1, + 10, + Percent::from_percent(50), + 8, + 0, + 0 + ), + Error::::CannotDelegateLessThanOrEqualToLowestBottomWhenFull + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_can_delegate_if_greater_than_lowest_bottom() { + ExtBuilder::default() + .with_balances(vec![ + (1, 20), + (2, 10), + (3, 10), + (4, 10), + (5, 10), + (6, 10), + (7, 10), + (8, 10), + (9, 10), + (10, 10), + (11, 11), + ]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![ + (2, 1, 10), + (3, 1, 10), + (4, 1, 10), + (5, 1, 10), + (6, 1, 10), + (8, 1, 10), + (9, 1, 10), + (10, 1, 10), + ]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(11), + 1, + 11, + Percent::from_percent(50), + 8, + 0, + 0 + )); + assert_events_emitted!(Event::DelegationKicked { delegator: 10, candidate: 1, unstaked_amount: 10 }); + assert_events_emitted!(Event::DelegatorLeft { delegator: 10, unstaked_amount: 10 }); + }); +} + +#[test] +fn test_delegate_with_auto_compound_can_still_delegate_to_other_if_leaving() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 20), (3, 20)]) + .with_candidates(vec![(1, 20), (3, 20)]) + .with_delegations(vec![(2, 1, 10)]) + .build() + .execute_with(|| { + assert_ok!(ParachainStaking::schedule_leave_delegators(RuntimeOrigin::signed(2))); + assert_ok!(ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 3, + 10, + Percent::from_percent(50), + 0, + 0, + 1 + ),); + }); +} + +#[test] +fn test_delegate_with_auto_compound_cannot_delegate_if_candidate() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 30)]) + .with_candidates(vec![(1, 20), (2, 20)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + 0, + 0, + 0 + ), + Error::::CandidateExists + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_cannot_delegate_if_already_delegated() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 30)]) + .with_candidates(vec![(1, 20)]) + .with_delegations(vec![(2, 1, 20)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 1, + 10, + Percent::from_percent(50), + 0, + 1, + 1 + ), + Error::::AlreadyDelegatedCandidate + ); + }); +} + +#[test] +fn test_delegate_with_auto_compound_cannot_delegate_more_than_max_delegations() { + ExtBuilder::default() + .with_balances(vec![(1, 20), (2, 50), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_candidates(vec![(1, 20), (3, 20), (4, 20), (5, 20), (6, 20)]) + .with_delegations(vec![(2, 1, 10), (2, 3, 10), (2, 4, 10), (2, 5, 10)]) + .build() + .execute_with(|| { + assert_noop!( + ParachainStaking::delegate_with_auto_compound( + RuntimeOrigin::signed(2), + 6, + 10, + Percent::from_percent(50), + 0, + 0, + 4 + ), + Error::::ExceedMaxDelegationsPerDelegator, + ); + }); +} + +#[test] +fn test_delegate_skips_auto_compound_storage_but_emits_event_for_zero_auto_compound() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 20), (3, 30)]) + .with_candidates(vec![(1, 30)]) + .with_auto_compounding_delegations(vec![(3, 1, 10, Percent::from_percent(50))]) + .build() + .execute_with(|| { + // We already have an auto-compounding delegation from 3 -> 1, so the hint validation + // would cause a failure if the auto-compounding isn't skipped properly. + assert_ok!(ParachainStaking::delegate(RuntimeOrigin::signed(2), 1, 10, 1, 0,)); + assert_eq!(1, ParachainStaking::auto_compounding_delegations(&1).len(),); + assert_events_eq!(Event::Delegation { + delegator: 2, + locked_amount: 10, + candidate: 1, + delegator_position: DelegatorAdded::AddedToTop { new_total: 50 }, + auto_compound: Percent::zero(), + }); + }); +} + +#[test] +fn test_on_initialize_weights() { + use crate::{ + mock::System, + weights::{SubstrateWeight as PalletWeights, WeightInfo}, + *, + }; + use frame_support::{pallet_prelude::*, weights::constants::RocksDbWeight}; + + // generate balance, candidate, and delegation vecs to "fill" out delegations + let mut balances = Vec::new(); + let mut candidates = Vec::new(); + let mut delegations = Vec::new(); + + for collator in 1..30 { + balances.push((collator, 100)); + candidates.push((collator, 10)); + let starting_delegator = collator * 1000; + for delegator in starting_delegator..starting_delegator + 300 { + balances.push((delegator, 100)); + delegations.push((delegator, collator, 10)); + } + } + + ExtBuilder::default() + .with_balances(balances) + .with_candidates(candidates) + .with_delegations(delegations) + .build() + .execute_with(|| { + let weight = ParachainStaking::on_initialize(1); + + // TODO: build this with proper db reads/writes + assert_eq!(Weight::from_parts(304110000, 1832), weight); + + // roll to the end of the round, then run on_init again, we should see round change... + roll_to_round_end(3); + set_author(2, 1, 100); // must set some points for prepare_staking_payouts + let block = System::block_number() + 1; + let weight = ParachainStaking::on_initialize(block); + + // the total on_init weight during our round change. this number is taken from running + // the fn with a given weights.rs benchmark, so will need to be updated as benchmarks + // change. + // + // following this assertion, we add individual weights together to show that we can + // derive this number independently. + let expected_on_init = 2053713382; + assert_eq!(Weight::from_parts(expected_on_init, 42845), weight); + + // assemble weight manually to ensure it is well understood + let mut expected_weight = 0u64; + expected_weight += PalletWeights::::base_on_initialize().ref_time(); + expected_weight += PalletWeights::::prepare_staking_payouts().ref_time(); + + // TODO: this should be the same as >. I believe this relates to + // genesis building + let num_avg_delegations = 8; + expected_weight += + PalletWeights::::select_top_candidates(>::get(), num_avg_delegations) + .ref_time(); + // Round and Staked writes, done in on-round-change code block inside on_initialize() + expected_weight += RocksDbWeight::get().reads_writes(0, 2).ref_time(); + // more reads/writes manually accounted for for on_finalize + expected_weight += RocksDbWeight::get().reads_writes(3, 2).ref_time(); + + assert_eq!(Weight::from_parts(expected_weight, 42845), weight); + assert_eq!(expected_on_init, expected_weight); // magic number == independent accounting + }); +} + +#[test] +fn test_compute_top_candidates_is_stable() { + ExtBuilder::default() + .with_balances(vec![(1, 30), (2, 30), (3, 30), (4, 30), (5, 30), (6, 30)]) + .with_candidates(vec![(1, 30), (2, 30), (3, 30), (4, 30), (5, 30), (6, 30)]) + .build() + .execute_with(|| { + // There are 6 candidates with equal amount, but only 5 can be selected + assert_eq!(ParachainStaking::candidate_pool().0.len(), 6); + assert_eq!(ParachainStaking::total_selected(), 5); + // Returns the 5 candidates with greater AccountId, because they are iterated in reverse + assert_eq!(ParachainStaking::compute_top_candidates(), vec![2, 3, 4, 5, 6]); + }); +} diff --git a/pallets/parachain-staking/src/traits.rs b/pallets/parachain-staking/src/traits.rs new file mode 100644 index 0000000..208c134 --- /dev/null +++ b/pallets/parachain-staking/src/traits.rs @@ -0,0 +1,55 @@ +// 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 . + +//! traits for parachain-staking + +use frame_support::pallet_prelude::Weight; + +pub trait OnCollatorPayout { + fn on_collator_payout(for_round: crate::RoundIndex, collator_id: AccountId, amount: Balance) -> Weight; +} +impl OnCollatorPayout for () { + fn on_collator_payout(_for_round: crate::RoundIndex, _collator_id: AccountId, _amount: Balance) -> Weight { + Weight::zero() + } +} + +pub trait OnNewRound { + fn on_new_round(round_index: crate::RoundIndex) -> Weight; +} +impl OnNewRound for () { + fn on_new_round(_round_index: crate::RoundIndex) -> Weight { + Weight::zero() + } +} + +/// Defines the behavior to payout the collator's reward. +pub trait PayoutCollatorReward { + fn payout_collator_reward( + round_index: crate::RoundIndex, + collator_id: Runtime::AccountId, + amount: crate::BalanceOf, + ) -> Weight; +} + +/// Defines the default behavior for paying out the collator's reward. The amount is directly +/// deposited into the collator's account. +impl PayoutCollatorReward for () { + fn payout_collator_reward( + for_round: crate::RoundIndex, + collator_id: Runtime::AccountId, + amount: crate::BalanceOf, + ) -> Weight { + crate::Pallet::::mint_collator_reward(for_round, collator_id, amount) + } +} diff --git a/pallets/parachain-staking/src/types.rs b/pallets/parachain-staking/src/types.rs new file mode 100644 index 0000000..daeb9e0 --- /dev/null +++ b/pallets/parachain-staking/src/types.rs @@ -0,0 +1,1600 @@ +// 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 . +//! Types for parachain-staking + +use crate::{ + auto_compound::AutoCompoundDelegations, set::OrderedSet, BalanceOf, BottomDelegations, CandidateInfo, Config, + DelegatorState, Error, Event, HoldReason, Pallet, Round, RoundIndex, TopDelegations, Total, +}; +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{InspectHold, MutateHold}, + tokens::Precision, + }, +}; +use codec::{Decode, Encode}; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Saturating, Zero}, + Perbill, Percent, RuntimeDebug, +}; +use sp_std::{cmp::Ordering, collections::btree_map::BTreeMap, prelude::*}; + +pub struct CountedDelegations { + pub uncounted_stake: BalanceOf, + pub rewardable_delegations: Vec>>, +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Bond { + pub owner: AccountId, + pub amount: Balance, +} + +impl Default for Bond { + fn default() -> Bond { + Bond { + owner: A::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"), + amount: B::default(), + } + } +} + +impl Bond { + pub fn from_owner(owner: A) -> Self { + Bond { owner, amount: B::default() } + } +} + +impl Eq for Bond {} + +impl Ord for Bond { + fn cmp(&self, other: &Self) -> Ordering { + self.owner.cmp(&other.owner) + } +} + +impl PartialOrd for Bond { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Bond { + fn eq(&self, other: &Self) -> bool { + self.owner == other.owner + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +/// The activity status of the collator +#[derive(Default)] +pub enum CollatorStatus { + /// Committed to be online and producing valid blocks (not equivocating) + #[default] + Active, + /// Temporarily inactive and excused for inactivity + Idle, + /// Bonded until the inner round + Leaving(RoundIndex), +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct BondWithAutoCompound { + pub owner: AccountId, + pub amount: Balance, + pub auto_compound: Percent, +} + +impl Default for BondWithAutoCompound { + fn default() -> BondWithAutoCompound { + BondWithAutoCompound { + owner: A::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"), + amount: B::default(), + auto_compound: Percent::zero(), + } + } +} + +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +/// Snapshot of collator state at the start of the round for which they are selected +pub struct CollatorSnapshot { + /// The total value locked by the collator. + pub bond: Balance, + + /// The rewardable delegations. This list is a subset of total delegators, where certain + /// delegators are adjusted based on their scheduled + /// [DelegationChange::Revoke] or [DelegationChange::Decrease] action. + pub delegations: Vec>, + + /// The total counted value locked for the collator, including the self bond + total staked by + /// top delegators. + pub total: Balance, +} + +impl PartialEq for CollatorSnapshot { + fn eq(&self, other: &Self) -> bool { + let must_be_true = self.bond == other.bond && self.total == other.total; + if !must_be_true { + return false; + } + for ( + BondWithAutoCompound { owner: o1, amount: a1, auto_compound: c1 }, + BondWithAutoCompound { owner: o2, amount: a2, auto_compound: c2 }, + ) in self.delegations.iter().zip(other.delegations.iter()) + { + if o1 != o2 || a1 != a2 || c1 != c2 { + return false; + } + } + true + } +} + +impl Default for CollatorSnapshot { + fn default() -> CollatorSnapshot { + CollatorSnapshot { bond: B::default(), delegations: Vec::new(), total: B::default() } + } +} + +#[derive(Default, Encode, Decode, RuntimeDebug, TypeInfo)] +/// Info needed to make delayed payments to stakers after round end +pub struct DelayedPayout { + /// Total round reward (result of compute_issuance() at round end) + pub round_issuance: Balance, + /// The total inflation paid this round to stakers (e.g. less parachain bond fund) + pub total_staking_reward: Balance, + /// Snapshot of collator commission rate at the end of the round + pub collator_commission: Perbill, +} + +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +/// DEPRECATED +/// Collator state with commission fee, bonded stake, and delegations +pub struct Collator2 { + /// The account of this collator + pub id: AccountId, + /// This collator's self stake. + pub bond: Balance, + /// Set of all nominator AccountIds (to prevent >1 nomination per AccountId) + pub nominators: OrderedSet, + /// Top T::MaxDelegatorsPerCollator::get() nominators, ordered greatest to least + pub top_nominators: Vec>, + /// Bottom nominators (unbounded), ordered least to greatest + pub bottom_nominators: Vec>, + /// Sum of top delegations + self.bond + pub total_counted: Balance, + /// Sum of all delegations + self.bond = (total_counted + uncounted) + pub total_backing: Balance, + /// Current status of the collator + pub state: CollatorStatus, +} + +impl From> for CollatorCandidate { + fn from(other: Collator2) -> CollatorCandidate { + CollatorCandidate { + id: other.id, + bond: other.bond, + delegators: other.nominators, + top_delegations: other.top_nominators, + bottom_delegations: other.bottom_nominators, + total_counted: other.total_counted, + total_backing: other.total_backing, + request: None, + state: other.state, + } + } +} + +#[derive(PartialEq, Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo)] +/// Request scheduled to change the collator candidate self-bond +pub struct CandidateBondLessRequest { + pub amount: Balance, + pub when_executable: RoundIndex, +} + +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +/// DEPRECATED, replaced by `CandidateMetadata` and two storage instances of `Delegations` +/// Collator candidate state with self bond + delegations +pub struct CollatorCandidate { + /// The account of this collator + pub id: AccountId, + /// This collator's self stake. + pub bond: Balance, + /// Set of all delegator AccountIds (to prevent >1 delegation per AccountId) + pub delegators: OrderedSet, + /// Top T::MaxDelegatorsPerCollator::get() delegations, ordered greatest to least + pub top_delegations: Vec>, + /// Bottom delegations (unbounded), ordered least to greatest + pub bottom_delegations: Vec>, + /// Sum of top delegations + self.bond + pub total_counted: Balance, + /// Sum of all delegations + self.bond = (total_counted + uncounted) + pub total_backing: Balance, + /// Maximum 1 pending request to decrease candidate self bond at any given time + pub request: Option>, + /// Current status of the collator + pub state: CollatorStatus, +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +/// Type for top and bottom delegation storage item +pub struct Delegations { + pub delegations: Vec>, + pub total: Balance, +} + +impl Default for Delegations { + fn default() -> Delegations { + Delegations { delegations: Vec::new(), total: B::default() } + } +} + +impl Delegations { + pub fn sort_greatest_to_least(&mut self) { + self.delegations.sort_by(|a, b| b.amount.cmp(&a.amount)); + } + + /// Insert sorted greatest to least and increase .total accordingly + /// Insertion respects first come first serve so new delegations are pushed after existing + /// delegations if the amount is the same + pub fn insert_sorted_greatest_to_least(&mut self, delegation: Bond) { + self.total = self.total.saturating_add(delegation.amount); + // if delegations nonempty && last_element == delegation.amount => push input and return + if !self.delegations.is_empty() { + // if last_element == delegation.amount => push the delegation and return early + if self.delegations[self.delegations.len() - 1].amount == delegation.amount { + self.delegations.push(delegation); + // early return + return; + } + } + // else binary search insertion + match self.delegations.binary_search_by(|x| delegation.amount.cmp(&x.amount)) { + // sorted insertion on sorted vec + // enforces first come first serve for equal bond amounts + Ok(i) => { + let mut new_index = i + 1; + while new_index <= (self.delegations.len() - 1) { + if self.delegations[new_index].amount == delegation.amount { + new_index = new_index.saturating_add(1); + } else { + self.delegations.insert(new_index, delegation); + return; + } + } + self.delegations.push(delegation) + }, + Err(i) => self.delegations.insert(i, delegation), + } + } + + /// Return the capacity status for top delegations + pub fn top_capacity(&self) -> CapacityStatus { + match &self.delegations { + x if x.len() as u32 >= T::MaxTopDelegationsPerCandidate::get() => CapacityStatus::Full, + x if x.is_empty() => CapacityStatus::Empty, + _ => CapacityStatus::Partial, + } + } + + /// Return the capacity status for bottom delegations + pub fn bottom_capacity(&self) -> CapacityStatus { + match &self.delegations { + x if x.len() as u32 >= T::MaxBottomDelegationsPerCandidate::get() => CapacityStatus::Full, + x if x.is_empty() => CapacityStatus::Empty, + _ => CapacityStatus::Partial, + } + } + + /// Return last delegation amount without popping the delegation + pub fn lowest_delegation_amount(&self) -> Balance { + self.delegations.last().map(|x| x.amount).unwrap_or(Balance::zero()) + } + + /// Return highest delegation amount + pub fn highest_delegation_amount(&self) -> Balance { + self.delegations.first().map(|x| x.amount).unwrap_or(Balance::zero()) + } +} + +#[derive(PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +/// Capacity status for top or bottom delegations +pub enum CapacityStatus { + /// Reached capacity + Full, + /// Empty aka contains no delegations + Empty, + /// Partially full (nonempty and not full) + Partial, +} + +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +/// All candidate info except the top and bottom delegations +pub struct CandidateMetadata { + /// This candidate's self bond amount + pub bond: Balance, + /// Total number of delegations to this candidate + pub delegation_count: u32, + /// Self bond + sum of top delegations + pub total_counted: Balance, + /// The smallest top delegation amount + pub lowest_top_delegation_amount: Balance, + /// The highest bottom delegation amount + pub highest_bottom_delegation_amount: Balance, + /// The smallest bottom delegation amount + pub lowest_bottom_delegation_amount: Balance, + /// Capacity status for top delegations + pub top_capacity: CapacityStatus, + /// Capacity status for bottom delegations + pub bottom_capacity: CapacityStatus, + /// Maximum 1 pending request to decrease candidate self bond at any given time + pub request: Option>, + /// Current status of the collator + pub status: CollatorStatus, +} + +impl< + Balance: Copy + + Zero + + PartialOrd + + sp_std::ops::AddAssign + + sp_std::ops::SubAssign + + sp_std::ops::Sub + + sp_std::fmt::Debug + + Saturating, + > CandidateMetadata +{ + pub fn new(bond: Balance) -> Self { + CandidateMetadata { + bond, + delegation_count: 0u32, + total_counted: bond, + lowest_top_delegation_amount: Zero::zero(), + highest_bottom_delegation_amount: Zero::zero(), + lowest_bottom_delegation_amount: Zero::zero(), + top_capacity: CapacityStatus::Empty, + bottom_capacity: CapacityStatus::Empty, + request: None, + status: CollatorStatus::Active, + } + } + + pub fn is_active(&self) -> bool { + matches!(self.status, CollatorStatus::Active) + } + + pub fn is_leaving(&self) -> bool { + matches!(self.status, CollatorStatus::Leaving(_)) + } + + pub fn schedule_leave(&mut self) -> Result<(RoundIndex, RoundIndex), DispatchError> { + ensure!(!self.is_leaving(), Error::::CandidateAlreadyLeaving); + let now = >::get().current; + let when = now + T::LeaveCandidatesDelay::get(); + self.status = CollatorStatus::Leaving(when); + Ok((now, when)) + } + + pub fn can_leave(&self) -> DispatchResult { + if let CollatorStatus::Leaving(when) = self.status { + ensure!(>::get().current >= when, Error::::CandidateCannotLeaveYet); + Ok(()) + } else { + Err(Error::::CandidateNotLeaving.into()) + } + } + + pub fn go_offline(&mut self) { + self.status = CollatorStatus::Idle; + } + + pub fn go_online(&mut self) { + self.status = CollatorStatus::Active; + } + + pub fn bond_more(&mut self, who: T::AccountId, more: Balance) -> DispatchResult + where + BalanceOf: From, + { + ensure!(>::get_collator_stakable_free_balance(&who) >= more.into(), Error::::InsufficientBalance); + let new_total = >::get().saturating_add(more.into()); + >::put(new_total); + self.bond = self.bond.saturating_add(more); + T::Currency::hold(&HoldReason::StakingCollator.into(), &who, more.into())?; + self.total_counted = self.total_counted.saturating_add(more); + >::deposit_event(Event::CandidateBondedMore { + candidate: who, + amount: more.into(), + new_total_bond: self.bond.into(), + }); + Ok(()) + } + + /// Schedule executable decrease of collator candidate self bond + /// Returns the round at which the collator can execute the pending request + pub fn schedule_bond_less(&mut self, less: Balance) -> Result + where + BalanceOf: Into, + { + // ensure no pending request + ensure!(self.request.is_none(), Error::::PendingCandidateRequestAlreadyExists); + // ensure bond above min after decrease + ensure!(self.bond > less, Error::::CandidateBondBelowMin); + ensure!(self.bond - less >= T::MinCandidateStk::get().into(), Error::::CandidateBondBelowMin); + let when_executable = >::get().current + T::CandidateBondLessDelay::get(); + self.request = Some(CandidateBondLessRequest { amount: less, when_executable }); + Ok(when_executable) + } + + /// Execute pending request to decrease the collator self bond + /// Returns the event to be emitted + pub fn execute_bond_less(&mut self, who: T::AccountId) -> DispatchResult + where + BalanceOf: From, + { + let request = self.request.ok_or(Error::::PendingCandidateRequestsDNE)?; + ensure!(request.when_executable <= >::get().current, Error::::PendingCandidateRequestNotDueYet); + let new_total_staked = >::get().saturating_sub(request.amount.into()); + >::put(new_total_staked); + // Arithmetic assumptions are self.bond > less && self.bond - less > CollatorMinBond + // (assumptions enforced by `schedule_bond_less`; if storage corrupts, must re-verify) + self.bond = self.bond.saturating_sub(request.amount); + T::Currency::release(&HoldReason::StakingCollator.into(), &who, request.amount.into(), Precision::Exact)?; + self.total_counted = self.total_counted.saturating_sub(request.amount); + let event = Event::CandidateBondedLess { + candidate: who.clone(), + amount: request.amount.into(), + new_bond: self.bond.into(), + }; + // reset s.t. no pending request + self.request = None; + // update candidate pool value because it must change if self bond changes + if self.is_active() { + Pallet::::update_active(who, self.total_counted.into()); + } + Pallet::::deposit_event(event); + Ok(()) + } + + /// Cancel candidate bond less request + pub fn cancel_bond_less(&mut self, who: T::AccountId) -> DispatchResult + where + BalanceOf: From, + { + let request = self.request.ok_or(Error::::PendingCandidateRequestsDNE)?; + let event = Event::CancelledCandidateBondLess { + candidate: who, + amount: request.amount.into(), + execute_round: request.when_executable, + }; + self.request = None; + Pallet::::deposit_event(event); + Ok(()) + } + + /// Reset top delegations metadata + pub fn reset_top_data( + &mut self, + candidate: T::AccountId, + top_delegations: &Delegations>, + ) where + BalanceOf: Into + From, + { + self.lowest_top_delegation_amount = top_delegations.lowest_delegation_amount().into(); + self.top_capacity = top_delegations.top_capacity::(); + let old_total_counted = self.total_counted; + self.total_counted = self.bond.saturating_add(top_delegations.total.into()); + // CandidatePool value for candidate always changes if top delegations total changes + // so we moved the update into this function to deduplicate code and patch a bug that + // forgot to apply the update when increasing top delegation + if old_total_counted != self.total_counted && self.is_active() { + Pallet::::update_active(candidate, self.total_counted.into()); + } + } + + /// Reset bottom delegations metadata + pub fn reset_bottom_data(&mut self, bottom_delegations: &Delegations>) + where + BalanceOf: Into, + { + self.lowest_bottom_delegation_amount = bottom_delegations.lowest_delegation_amount().into(); + self.highest_bottom_delegation_amount = bottom_delegations.highest_delegation_amount().into(); + self.bottom_capacity = bottom_delegations.bottom_capacity::(); + } + + /// Add delegation + /// Returns whether delegator was added and an optional negative total counted remainder + /// for if a bottom delegation was kicked + /// MUST ensure no delegation exists for this candidate in the `DelegatorState` before call + pub fn add_delegation( + &mut self, + candidate: &T::AccountId, + delegation: Bond>, + ) -> Result<(DelegatorAdded, Option), DispatchError> + where + BalanceOf: Into + From, + { + let mut less_total_staked = None; + let delegator_added = match self.top_capacity { + CapacityStatus::Full => { + // top is full, insert into top iff the lowest_top < amount + if self.lowest_top_delegation_amount < delegation.amount.into() { + // bumps lowest top to the bottom inside this function call + less_total_staked = self.add_top_delegation::(candidate, delegation); + DelegatorAdded::AddedToTop { new_total: self.total_counted } + } else { + // if bottom is full, only insert if greater than lowest bottom (which will + // be bumped out) + if matches!(self.bottom_capacity, CapacityStatus::Full) { + ensure!( + delegation.amount.into() > self.lowest_bottom_delegation_amount, + Error::::CannotDelegateLessThanOrEqualToLowestBottomWhenFull + ); + // need to subtract from total staked + less_total_staked = Some(self.lowest_bottom_delegation_amount); + } + // insert into bottom + self.add_bottom_delegation::(false, candidate, delegation); + DelegatorAdded::AddedToBottom + } + }, + // top is either empty or partially full + _ => { + self.add_top_delegation::(candidate, delegation); + DelegatorAdded::AddedToTop { new_total: self.total_counted } + }, + }; + Ok((delegator_added, less_total_staked)) + } + + /// Add delegation to top delegation + /// Returns Option + /// Only call if lowest top delegation is less than delegation.amount || !top_full + pub fn add_top_delegation( + &mut self, + candidate: &T::AccountId, + delegation: Bond>, + ) -> Option + where + BalanceOf: Into + From, + { + let mut less_total_staked = None; + let mut top_delegations = + >::get(candidate).expect("CandidateInfo existence => TopDelegations existence"); + let max_top_delegations_per_candidate = T::MaxTopDelegationsPerCandidate::get(); + if top_delegations.delegations.len() as u32 == max_top_delegations_per_candidate { + // pop lowest top delegation + let new_bottom_delegation = top_delegations.delegations.pop().expect(""); + top_delegations.total = top_delegations.total.saturating_sub(new_bottom_delegation.amount); + if matches!(self.bottom_capacity, CapacityStatus::Full) { + less_total_staked = Some(self.lowest_bottom_delegation_amount); + } + self.add_bottom_delegation::(true, candidate, new_bottom_delegation); + } + // insert into top + top_delegations.insert_sorted_greatest_to_least(delegation); + // update candidate info + self.reset_top_data::(candidate.clone(), &top_delegations); + if less_total_staked.is_none() { + // only increment delegation count if we are not kicking a bottom delegation + self.delegation_count = self.delegation_count.saturating_add(1u32); + } + >::insert(candidate, top_delegations); + less_total_staked + } + + /// Add delegation to bottom delegations + /// Check before call that if capacity is full, inserted delegation is higher than lowest + /// bottom delegation (and if so, need to adjust the total storage item) + /// CALLER MUST ensure(lowest_bottom_to_be_kicked.amount < delegation.amount) + pub fn add_bottom_delegation( + &mut self, + bumped_from_top: bool, + candidate: &T::AccountId, + delegation: Bond>, + ) where + BalanceOf: Into + From, + { + let mut bottom_delegations = + >::get(candidate).expect("CandidateInfo existence => BottomDelegations existence"); + // if bottom is full, kick the lowest bottom (which is expected to be lower than input + // as per check) + let increase_delegation_count = + if bottom_delegations.delegations.len() as u32 == T::MaxBottomDelegationsPerCandidate::get() { + let lowest_bottom_to_be_kicked = bottom_delegations + .delegations + .pop() + .expect("if at full capacity (>0), then >0 bottom delegations exist; qed"); + // EXPECT lowest_bottom_to_be_kicked.amount < delegation.amount enforced by caller + // if lowest_bottom_to_be_kicked.amount == delegation.amount, we will still kick + // the lowest bottom to enforce first come first served + bottom_delegations.total = bottom_delegations.total.saturating_sub(lowest_bottom_to_be_kicked.amount); + // update delegator state + // total staked is updated via propagation of lowest bottom delegation amount prior + // to call + let mut delegator_state = >::get(&lowest_bottom_to_be_kicked.owner) + .expect("Delegation existence => DelegatorState existence"); + let leaving = delegator_state.delegations.0.len() == 1usize; + delegator_state.rm_delegation::(candidate); + >::delegation_remove_request_with_state( + candidate, + &lowest_bottom_to_be_kicked.owner, + &mut delegator_state, + ); + >::remove_auto_compound(candidate, &lowest_bottom_to_be_kicked.owner); + + Pallet::::deposit_event(Event::DelegationKicked { + delegator: lowest_bottom_to_be_kicked.owner.clone(), + candidate: candidate.clone(), + unstaked_amount: lowest_bottom_to_be_kicked.amount, + }); + if leaving { + >::remove(&lowest_bottom_to_be_kicked.owner); + Pallet::::deposit_event(Event::DelegatorLeft { + delegator: lowest_bottom_to_be_kicked.owner, + unstaked_amount: lowest_bottom_to_be_kicked.amount, + }); + } else { + >::insert(&lowest_bottom_to_be_kicked.owner, delegator_state); + } + false + } else { + !bumped_from_top + }; + // only increase delegation count if new bottom delegation (1) doesn't come from top && + // (2) doesn't pop the lowest delegation from the bottom + if increase_delegation_count { + self.delegation_count = self.delegation_count.saturating_add(1u32); + } + bottom_delegations.insert_sorted_greatest_to_least(delegation); + self.reset_bottom_data::(&bottom_delegations); + >::insert(candidate, bottom_delegations); + } + + /// Remove delegation + /// Removes from top if amount is above lowest top or top is not full + /// Return Ok(if_total_counted_changed) + pub fn rm_delegation_if_exists( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + amount: Balance, + ) -> Result + where + BalanceOf: Into + From, + { + let amount_geq_lowest_top = amount >= self.lowest_top_delegation_amount; + let top_is_not_full = !matches!(self.top_capacity, CapacityStatus::Full); + let lowest_top_eq_highest_bottom = self.lowest_top_delegation_amount == self.highest_bottom_delegation_amount; + let delegation_dne_err: DispatchError = Error::::DelegationDNE.into(); + if top_is_not_full || (amount_geq_lowest_top && !lowest_top_eq_highest_bottom) { + self.rm_top_delegation::(candidate, delegator) + } else if amount_geq_lowest_top && lowest_top_eq_highest_bottom { + let result = self.rm_top_delegation::(candidate, delegator.clone()); + if result == Err(delegation_dne_err) { + // worst case removal + self.rm_bottom_delegation::(candidate, delegator) + } else { + result + } + } else { + self.rm_bottom_delegation::(candidate, delegator) + } + } + + /// Remove top delegation, bumps top bottom delegation if exists + pub fn rm_top_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + ) -> Result + where + BalanceOf: Into + From, + { + let old_total_counted = self.total_counted; + // remove top delegation + let mut top_delegations = + >::get(candidate).expect("CandidateInfo exists => TopDelegations exists"); + let mut actual_amount_option: Option> = None; + top_delegations.delegations = top_delegations + .delegations + .clone() + .into_iter() + .filter(|d| { + if d.owner != delegator { + true + } else { + actual_amount_option = Some(d.amount); + false + } + }) + .collect(); + let actual_amount = actual_amount_option.ok_or(Error::::DelegationDNE)?; + top_delegations.total = top_delegations.total.saturating_sub(actual_amount); + // if bottom nonempty => bump top bottom to top + if !matches!(self.bottom_capacity, CapacityStatus::Empty) { + let mut bottom_delegations = + >::get(candidate).expect("bottom is nonempty as just checked"); + // expect already stored greatest to least by bond amount + let highest_bottom_delegation = bottom_delegations.delegations.remove(0); + bottom_delegations.total = bottom_delegations.total.saturating_sub(highest_bottom_delegation.amount); + self.reset_bottom_data::(&bottom_delegations); + >::insert(candidate, bottom_delegations); + // insert highest bottom into top delegations + top_delegations.insert_sorted_greatest_to_least(highest_bottom_delegation); + } + // update candidate info + self.reset_top_data::(candidate.clone(), &top_delegations); + self.delegation_count = self.delegation_count.saturating_sub(1u32); + >::insert(candidate, top_delegations); + // return whether total counted changed + Ok(old_total_counted == self.total_counted) + } + + /// Remove bottom delegation + /// Returns if_total_counted_changed: bool + pub fn rm_bottom_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + ) -> Result + where + BalanceOf: Into, + { + // remove bottom delegation + let mut bottom_delegations = + >::get(candidate).expect("CandidateInfo exists => BottomDelegations exists"); + let mut actual_amount_option: Option> = None; + bottom_delegations.delegations = bottom_delegations + .delegations + .clone() + .into_iter() + .filter(|d| { + if d.owner != delegator { + true + } else { + actual_amount_option = Some(d.amount); + false + } + }) + .collect(); + let actual_amount = actual_amount_option.ok_or(Error::::DelegationDNE)?; + bottom_delegations.total = bottom_delegations.total.saturating_sub(actual_amount); + // update candidate info + self.reset_bottom_data::(&bottom_delegations); + self.delegation_count = self.delegation_count.saturating_sub(1u32); + >::insert(candidate, bottom_delegations); + Ok(false) + } + + /// Increase delegation amount + pub fn increase_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + bond: BalanceOf, + more: BalanceOf, + ) -> Result + where + BalanceOf: Into + From, + { + let lowest_top_eq_highest_bottom = self.lowest_top_delegation_amount == self.highest_bottom_delegation_amount; + let bond_geq_lowest_top = bond.into() >= self.lowest_top_delegation_amount; + let delegation_dne_err: DispatchError = Error::::DelegationDNE.into(); + if bond_geq_lowest_top && !lowest_top_eq_highest_bottom { + // definitely in top + self.increase_top_delegation::(candidate, delegator, more) + } else if bond_geq_lowest_top && lowest_top_eq_highest_bottom { + // update top but if error then update bottom (because could be in bottom because + // lowest_top_eq_highest_bottom) + let result = self.increase_top_delegation::(candidate, delegator.clone(), more); + if result == Err(delegation_dne_err) { + self.increase_bottom_delegation::(candidate, delegator, bond, more) + } else { + result + } + } else { + self.increase_bottom_delegation::(candidate, delegator, bond, more) + } + } + + /// Increase top delegation + pub fn increase_top_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + more: BalanceOf, + ) -> Result + where + BalanceOf: Into + From, + { + let mut top_delegations = + >::get(candidate).expect("CandidateInfo exists => TopDelegations exists"); + let mut in_top = false; + top_delegations.delegations = top_delegations + .delegations + .clone() + .into_iter() + .map(|d| { + if d.owner != delegator { + d + } else { + in_top = true; + let new_amount = d.amount.saturating_add(more); + Bond { owner: d.owner, amount: new_amount } + } + }) + .collect(); + ensure!(in_top, Error::::DelegationDNE); + top_delegations.total = top_delegations.total.saturating_add(more); + top_delegations.sort_greatest_to_least(); + self.reset_top_data::(candidate.clone(), &top_delegations); + >::insert(candidate, top_delegations); + Ok(true) + } + + /// Increase bottom delegation + pub fn increase_bottom_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + bond: BalanceOf, + more: BalanceOf, + ) -> Result + where + BalanceOf: Into + From, + { + let mut bottom_delegations = >::get(candidate).ok_or(Error::::CandidateDNE)?; + let mut delegation_option: Option>> = None; + let in_top_after = if (bond.saturating_add(more)).into() > self.lowest_top_delegation_amount { + // bump it from bottom + bottom_delegations.delegations = bottom_delegations + .delegations + .clone() + .into_iter() + .filter(|d| { + if d.owner != delegator { + true + } else { + delegation_option = + Some(Bond { owner: d.owner.clone(), amount: d.amount.saturating_add(more) }); + false + } + }) + .collect(); + let delegation = delegation_option.ok_or(Error::::DelegationDNE)?; + bottom_delegations.total = bottom_delegations.total.saturating_sub(bond); + // add it to top + let mut top_delegations = + >::get(candidate).expect("CandidateInfo existence => TopDelegations existence"); + // if top is full, pop lowest top + if matches!(top_delegations.top_capacity::(), CapacityStatus::Full) { + // pop lowest top delegation + let new_bottom_delegation = + top_delegations.delegations.pop().expect("Top capacity full => Exists at least 1 top delegation"); + top_delegations.total = top_delegations.total.saturating_sub(new_bottom_delegation.amount); + bottom_delegations.insert_sorted_greatest_to_least(new_bottom_delegation); + } + // insert into top + top_delegations.insert_sorted_greatest_to_least(delegation); + self.reset_top_data::(candidate.clone(), &top_delegations); + >::insert(candidate, top_delegations); + true + } else { + let mut in_bottom = false; + // just increase the delegation + bottom_delegations.delegations = bottom_delegations + .delegations + .clone() + .into_iter() + .map(|d| { + if d.owner != delegator { + d + } else { + in_bottom = true; + Bond { owner: d.owner, amount: d.amount.saturating_add(more) } + } + }) + .collect(); + ensure!(in_bottom, Error::::DelegationDNE); + bottom_delegations.total = bottom_delegations.total.saturating_add(more); + bottom_delegations.sort_greatest_to_least(); + false + }; + self.reset_bottom_data::(&bottom_delegations); + >::insert(candidate, bottom_delegations); + Ok(in_top_after) + } + + /// Decrease delegation + pub fn decrease_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + bond: Balance, + less: BalanceOf, + ) -> Result + where + BalanceOf: Into + From, + { + let lowest_top_eq_highest_bottom = self.lowest_top_delegation_amount == self.highest_bottom_delegation_amount; + let bond_geq_lowest_top = bond >= self.lowest_top_delegation_amount; + let delegation_dne_err: DispatchError = Error::::DelegationDNE.into(); + if bond_geq_lowest_top && !lowest_top_eq_highest_bottom { + // definitely in top + self.decrease_top_delegation::(candidate, delegator, bond.into(), less) + } else if bond_geq_lowest_top && lowest_top_eq_highest_bottom { + // update top but if error then update bottom (because could be in bottom because + // lowest_top_eq_highest_bottom) + let result = self.decrease_top_delegation::(candidate, delegator.clone(), bond.into(), less); + if result == Err(delegation_dne_err) { + self.decrease_bottom_delegation::(candidate, delegator, less) + } else { + result + } + } else { + self.decrease_bottom_delegation::(candidate, delegator, less) + } + } + + /// Decrease top delegation + pub fn decrease_top_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + bond: BalanceOf, + less: BalanceOf, + ) -> Result + where + BalanceOf: Into + From, + { + // The delegation after the `decrease-delegation` will be strictly less than the + // highest bottom delegation + let bond_after_less_than_highest_bottom = + bond.saturating_sub(less).into() < self.highest_bottom_delegation_amount; + // The top delegations is full and the bottom delegations has at least one delegation + let full_top_and_nonempty_bottom = + matches!(self.top_capacity, CapacityStatus::Full) && !matches!(self.bottom_capacity, CapacityStatus::Empty); + let mut top_delegations = >::get(candidate).ok_or(Error::::CandidateDNE)?; + let in_top_after = if bond_after_less_than_highest_bottom && full_top_and_nonempty_bottom { + let mut delegation_option: Option>> = None; + // take delegation from top + top_delegations.delegations = top_delegations + .delegations + .clone() + .into_iter() + .filter(|d| { + if d.owner != delegator { + true + } else { + top_delegations.total = top_delegations.total.saturating_sub(d.amount); + delegation_option = + Some(Bond { owner: d.owner.clone(), amount: d.amount.saturating_sub(less) }); + false + } + }) + .collect(); + let delegation = delegation_option.ok_or(Error::::DelegationDNE)?; + // pop highest bottom by reverse and popping + let mut bottom_delegations = + >::get(candidate).expect("CandidateInfo existence => BottomDelegations existence"); + let highest_bottom_delegation = bottom_delegations.delegations.remove(0); + bottom_delegations.total = bottom_delegations.total.saturating_sub(highest_bottom_delegation.amount); + // insert highest bottom into top + top_delegations.insert_sorted_greatest_to_least(highest_bottom_delegation); + // insert previous top into bottom + bottom_delegations.insert_sorted_greatest_to_least(delegation); + self.reset_bottom_data::(&bottom_delegations); + >::insert(candidate, bottom_delegations); + false + } else { + // keep it in the top + let mut is_in_top = false; + top_delegations.delegations = top_delegations + .delegations + .clone() + .into_iter() + .map(|d| { + if d.owner != delegator { + d + } else { + is_in_top = true; + Bond { owner: d.owner, amount: d.amount.saturating_sub(less) } + } + }) + .collect(); + ensure!(is_in_top, Error::::DelegationDNE); + top_delegations.total = top_delegations.total.saturating_sub(less); + top_delegations.sort_greatest_to_least(); + true + }; + self.reset_top_data::(candidate.clone(), &top_delegations); + >::insert(candidate, top_delegations); + Ok(in_top_after) + } + + /// Decrease bottom delegation + pub fn decrease_bottom_delegation( + &mut self, + candidate: &T::AccountId, + delegator: T::AccountId, + less: BalanceOf, + ) -> Result + where + BalanceOf: Into, + { + let mut bottom_delegations = + >::get(candidate).expect("CandidateInfo exists => BottomDelegations exists"); + let mut in_bottom = false; + bottom_delegations.delegations = bottom_delegations + .delegations + .clone() + .into_iter() + .map(|d| { + if d.owner != delegator { + d + } else { + in_bottom = true; + Bond { owner: d.owner, amount: d.amount.saturating_sub(less) } + } + }) + .collect(); + ensure!(in_bottom, Error::::DelegationDNE); + bottom_delegations.sort_greatest_to_least(); + self.reset_bottom_data::(&bottom_delegations); + >::insert(candidate, bottom_delegations); + Ok(false) + } +} + +// Temporary manual implementation for migration testing purposes +impl PartialEq for CollatorCandidate { + fn eq(&self, other: &Self) -> bool { + let must_be_true = self.id == other.id && + self.bond == other.bond && + self.total_counted == other.total_counted && + self.total_backing == other.total_backing && + self.request == other.request && + self.state == other.state; + if !must_be_true { + return false; + } + for (x, y) in self.delegators.0.iter().zip(other.delegators.0.iter()) { + if x != y { + return false; + } + } + for (Bond { owner: o1, amount: a1 }, Bond { owner: o2, amount: a2 }) in + self.top_delegations.iter().zip(other.top_delegations.iter()) + { + if o1 != o2 || a1 != a2 { + return false; + } + } + for (Bond { owner: o1, amount: a1 }, Bond { owner: o2, amount: a2 }) in + self.bottom_delegations.iter().zip(other.bottom_delegations.iter()) + { + if o1 != o2 || a1 != a2 { + return false; + } + } + true + } +} + +/// Convey relevant information describing if a delegator was added to the top or bottom +/// Delegations added to the top yield a new total +#[derive(Clone, Copy, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum DelegatorAdded { + AddedToTop { new_total: B }, + AddedToBottom, +} + +impl< + A: Ord + Clone + sp_std::fmt::Debug, + B: AtLeast32BitUnsigned + Ord + Copy + sp_std::ops::AddAssign + sp_std::ops::SubAssign + sp_std::fmt::Debug, + > CollatorCandidate +{ + pub fn is_active(&self) -> bool { + self.state == CollatorStatus::Active + } +} + +impl From> for CollatorSnapshot { + fn from(other: CollatorCandidate) -> CollatorSnapshot { + CollatorSnapshot { + bond: other.bond, + delegations: other + .top_delegations + .into_iter() + .map(|d| BondWithAutoCompound { owner: d.owner, amount: d.amount, auto_compound: Percent::zero() }) + .collect(), + total: other.total_counted, + } + } +} + +#[allow(deprecated)] +#[derive(Clone, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum DelegatorStatus { + /// Active with no scheduled exit + Active, + /// Schedule exit to revoke all ongoing delegations + #[deprecated(note = "must only be used for backwards compatibility reasons")] + Leaving(RoundIndex), +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +/// Delegator state +pub struct Delegator { + /// Delegator account + pub id: AccountId, + /// All current delegations + pub delegations: OrderedSet>, + /// Total balance locked for this delegator + pub total: Balance, + /// Sum of pending revocation amounts + bond less amounts + pub less_total: Balance, + /// Status for this delegator + pub status: DelegatorStatus, +} + +// Temporary manual implementation for migration testing purposes +impl PartialEq for Delegator { + fn eq(&self, other: &Self) -> bool { + let must_be_true = self.id == other.id && + self.total == other.total && + self.less_total == other.less_total && + self.status == other.status; + if !must_be_true { + return false; + } + for (Bond { owner: o1, amount: a1 }, Bond { owner: o2, amount: a2 }) in + self.delegations.0.iter().zip(other.delegations.0.iter()) + { + if o1 != o2 || a1 != a2 { + return false; + } + } + true + } +} + +impl< + AccountId: Ord + Clone, + Balance: Copy + + sp_std::ops::AddAssign + + sp_std::ops::Add + + sp_std::ops::SubAssign + + sp_std::ops::Sub + + Ord + + Zero + + Default + + Saturating, + > Delegator +{ + pub fn new(id: AccountId, collator: AccountId, amount: Balance) -> Self { + Delegator { + id, + delegations: OrderedSet::from(vec![Bond { owner: collator, amount }]), + total: amount, + less_total: Balance::zero(), + status: DelegatorStatus::Active, + } + } + + pub fn default_with_total(id: AccountId, amount: Balance) -> Self { + Delegator { + id, + total: amount, + delegations: OrderedSet::from(vec![]), + less_total: Balance::zero(), + status: DelegatorStatus::Active, + } + } + + pub fn total(&self) -> Balance { + self.total + } + + pub fn total_sub_if(&mut self, amount: Balance, check: F) -> DispatchResult + where + T: Config, + T::AccountId: From, + BalanceOf: From, + F: Fn(Balance) -> DispatchResult, + { + let total = self.total.saturating_sub(amount); + check(total)?; + self.total = total; + self.adjust_bond_lock::(BondAdjust::Decrease)?; + Ok(()) + } + + pub fn total_add(&mut self, amount: Balance) -> DispatchResult + where + T: Config, + T::AccountId: From, + BalanceOf: From, + { + self.total = self.total.saturating_add(amount); + self.adjust_bond_lock::(BondAdjust::Increase(amount))?; + Ok(()) + } + + pub fn total_sub(&mut self, amount: Balance) -> DispatchResult + where + T: Config, + T::AccountId: From, + BalanceOf: From, + { + self.total = self.total.saturating_sub(amount); + self.adjust_bond_lock::(BondAdjust::Decrease)?; + Ok(()) + } + + pub fn is_active(&self) -> bool { + matches!(self.status, DelegatorStatus::Active) + } + + pub fn add_delegation(&mut self, bond: Bond) -> bool { + let amt = bond.amount; + if self.delegations.insert(bond) { + self.total = self.total.saturating_add(amt); + true + } else { + false + } + } + + // Return Some(remaining balance), must be more than MinDelegatorStk + // Return None if delegation not found + pub fn rm_delegation(&mut self, collator: &AccountId) -> Option + where + BalanceOf: From, + T::AccountId: From, + { + let mut amt: Option = None; + let delegations = self + .delegations + .0 + .iter() + .filter_map(|x| { + if &x.owner == collator { + amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + if let Some(balance) = amt { + self.delegations = OrderedSet::from(delegations); + self.total_sub::(balance).expect("Decreasing lock cannot fail, qed"); + Some(self.total) + } else { + None + } + } + + /// Increases the delegation amount and returns `true` if the delegation is part of the + /// TopDelegations set, `false` otherwise. + pub fn increase_delegation( + &mut self, + candidate: AccountId, + amount: Balance, + ) -> Result + where + BalanceOf: From, + T::AccountId: From, + Delegator>: From>, + { + let delegator_id: T::AccountId = self.id.clone().into(); + let candidate_id: T::AccountId = candidate.clone().into(); + let balance_amt: BalanceOf = amount.into(); + // increase delegation + for x in &mut self.delegations.0 { + if x.owner == candidate { + let before_amount: BalanceOf = x.amount.into(); + x.amount = x.amount.saturating_add(amount); + self.total = self.total.saturating_add(amount); + self.adjust_bond_lock::(BondAdjust::Increase(amount))?; + + // update collator state delegation + let mut collator_state = >::get(&candidate_id).ok_or(Error::::CandidateDNE)?; + let before = collator_state.total_counted; + let in_top = collator_state.increase_delegation::( + &candidate_id, + delegator_id.clone(), + before_amount, + balance_amt, + )?; + let after = collator_state.total_counted; + if collator_state.is_active() && (before != after) { + Pallet::::update_active(candidate_id.clone(), after); + } + >::insert(&candidate_id, collator_state); + let new_total_staked = >::get().saturating_add(balance_amt); + >::put(new_total_staked); + let nom_st: Delegator> = self.clone().into(); + >::insert(&delegator_id, nom_st); + return Ok(in_top); + } + } + Err(Error::::DelegationDNE.into()) + } + + /// Updates the bond locks for this delegator. + /// + /// This will take the current self.total and ensure that a lock of the same amount is applied + /// and when increasing the bond lock will also ensure that the account has enough free balance. + /// + /// `additional_required_balance` should reflect the change to the amount that should be locked if + /// positive, 0 otherwise (e.g. `min(0, change_in_total_bond)`). This is necessary because it is + /// not possible to query the amount that is locked for a given lock id. + pub fn adjust_bond_lock(&mut self, additional_required_balance: BondAdjust) -> DispatchResult + where + BalanceOf: From, + T::AccountId: From, + { + match additional_required_balance { + BondAdjust::Increase(amount) => { + ensure!( + >::get_delegator_stakable_free_balance(&self.id.clone().into()) >= amount.into(), + Error::::InsufficientBalance, + ); + + // additional sanity check: shouldn't ever want to lock more than total + if amount > self.total { + log::warn!("LOGIC ERROR: request to reserve more than bond total"); + return Err(DispatchError::Other("Invalid additional_required_balance")); + } + }, + BondAdjust::Decrease => (), // do nothing on decrease + }; + + let total_bonded = T::Currency::balance_on_hold(&HoldReason::StakingDelegator.into(), &self.id.clone().into()); + + if total_bonded > self.total.into() { + let to_be_released = total_bonded.saturating_sub(self.total.into()); + T::Currency::release( + &HoldReason::StakingDelegator.into(), + &self.id.clone().into(), + to_be_released, + Precision::Exact, + )?; + } else { + let additional_hold = Into::::into(self.total).saturating_sub(total_bonded); + T::Currency::hold(&HoldReason::StakingDelegator.into(), &self.id.clone().into(), additional_hold.into())?; + } + + Ok(()) + } + + /// Retrieves the bond amount that a delegator has provided towards a collator. + /// Returns `None` if missing. + pub fn get_bond_amount(&self, collator: &AccountId) -> Option { + self.delegations.0.iter().find(|b| &b.owner == collator).map(|b| b.amount) + } +} + +pub mod deprecated { + #![allow(deprecated)] + + use super::*; + + #[deprecated(note = "use DelegationAction")] + #[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] + /// Changes requested by the delegator + /// - limit of 1 ongoing change per delegation + pub enum DelegationChange { + Revoke, + Decrease, + } + + #[deprecated(note = "use ScheduledRequest")] + #[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] + pub struct DelegationRequest { + pub collator: AccountId, + pub amount: Balance, + pub when_executable: RoundIndex, + pub action: DelegationChange, + } + + #[deprecated(note = "use DelegationScheduledRequests storage item")] + #[derive(Clone, Encode, PartialEq, Decode, RuntimeDebug, TypeInfo)] + /// Pending requests to mutate delegations for each delegator + pub struct PendingDelegationRequests { + /// Number of pending revocations (necessary for determining whether revoke is exit) + pub revocations_count: u32, + /// Map from collator -> Request (enforces at most 1 pending request per delegation) + pub requests: BTreeMap>, + /// Sum of pending revocation amounts + bond less amounts + pub less_total: Balance, + } + + impl Default for PendingDelegationRequests { + fn default() -> PendingDelegationRequests { + PendingDelegationRequests { revocations_count: 0u32, requests: BTreeMap::new(), less_total: B::zero() } + } + } + + impl< + A: Ord + Clone, + B: Zero + + Ord + + Copy + + Clone + + sp_std::ops::AddAssign + + sp_std::ops::Add + + sp_std::ops::SubAssign + + sp_std::ops::Sub + + Saturating, + > PendingDelegationRequests + { + /// New default (empty) pending requests + pub fn new() -> Self { + Self::default() + } + } + + #[deprecated(note = "use new crate::types::Delegator struct")] + #[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] + /// Delegator state + pub struct Delegator { + /// Delegator account + pub id: AccountId, + /// All current delegations + pub delegations: OrderedSet>, + /// Total balance locked for this delegator + pub total: Balance, + /// Requests to change delegations, relevant iff active + pub requests: PendingDelegationRequests, + /// Status for this delegator + pub status: DelegatorStatus, + } + + // CollatorSnapshot + + #[deprecated(note = "use CollatorSnapshot with BondWithAutoCompound delegations")] + #[derive(Encode, Decode, RuntimeDebug, TypeInfo)] + /// Snapshot of collator state at the start of the round for which they are selected + pub struct CollatorSnapshot { + /// The total value locked by the collator. + pub bond: Balance, + + /// The rewardable delegations. This list is a subset of total delegators, where certain + /// delegators are adjusted based on their scheduled + /// [DelegationChange::Revoke] or [DelegationChange::Decrease] action. + pub delegations: Vec>, + + /// The total counted value locked for the collator, including the self bond + total staked by + /// top delegators. + pub total: Balance, + } + + impl PartialEq for CollatorSnapshot { + fn eq(&self, other: &Self) -> bool { + let must_be_true = self.bond == other.bond && self.total == other.total; + if !must_be_true { + return false; + } + for (Bond { owner: o1, amount: a1 }, Bond { owner: o2, amount: a2 }) in + self.delegations.iter().zip(other.delegations.iter()) + { + if o1 != o2 || a1 != a2 { + return false; + } + } + true + } + } + + impl Default for CollatorSnapshot { + fn default() -> CollatorSnapshot { + CollatorSnapshot { bond: B::default(), delegations: Vec::new(), total: B::default() } + } + } +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +/// DEPRECATED in favor of Delegator +/// Nominator state +pub struct Nominator2 { + /// All current delegations + pub delegations: OrderedSet>, + /// Delegations scheduled to be revoked + pub revocations: OrderedSet, + /// Total balance locked for this nominator + pub total: Balance, + /// Total number of revocations scheduled to be executed + pub scheduled_revocations_count: u32, + /// Total amount to be unbonded once revocations are executed + pub scheduled_revocations_total: Balance, + /// Status for this nominator + pub status: DelegatorStatus, +} + +// /// Temporary function to migrate state +// pub(crate) fn migrate_nominator_to_delegator_state( +// id: T::AccountId, +// nominator: Nominator2>, +// ) -> Delegator> { +// Delegator { +// id, +// delegations: nominator.delegations, +// total: nominator.total, +// requests: PendingDelegationRequests::new(), +// status: nominator.status, +// } +// } + +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +/// The current round index and transition information +pub struct RoundInfo { + /// Current round index + pub current: RoundIndex, + /// The first block of the current round + pub first: BlockNumber, + /// The length of the current round in number of blocks + pub length: u32, +} +impl + sp_std::ops::Sub + From + PartialOrd> RoundInfo { + pub fn new(current: RoundIndex, first: B, length: u32) -> RoundInfo { + RoundInfo { current, first, length } + } + + /// Check if the round should be updated + pub fn should_update(&self, now: B) -> bool { + now - self.first >= self.length.into() + } + + /// New round + pub fn update(&mut self, now: B) { + self.current = self.current.saturating_add(1u32); + self.first = now; + } +} +impl + sp_std::ops::Sub + From + PartialOrd> Default + for RoundInfo +{ + fn default() -> RoundInfo { + RoundInfo::new(1u32, 1u32.into(), 20u32) + } +} + +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +/// Reserve information { account, percent_of_inflation } +pub struct ParachainBondConfig { + /// Account which receives funds intended for parachain bond + pub account: AccountId, + /// Percent of inflation set aside for parachain bond account + pub percent: Percent, +} +impl Default for ParachainBondConfig { + fn default() -> ParachainBondConfig { + ParachainBondConfig { + account: A::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"), + percent: Percent::zero(), + } + } +} + +pub enum BondAdjust { + Increase(Balance), + Decrease, +} diff --git a/pallets/parachain-staking/src/weights.rs b/pallets/parachain-staking/src/weights.rs new file mode 100644 index 0000000..d260df6 --- /dev/null +++ b/pallets/parachain-staking/src/weights.rs @@ -0,0 +1,1455 @@ +// 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 . + + +//! Autogenerated weights for `pallet_parachain_staking` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-04-15, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `ip-172-31-23-147`, CPU: `AMD EPYC 9R14` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("neuroweb-local")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/neuroweb +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --pallet=pallet-parachain-staking +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic +// * +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=pallets/parachain-staking/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_parachain_staking`. +pub trait WeightInfo { + fn set_staking_expectations() -> Weight; + fn set_inflation() -> Weight; + fn set_parachain_bond_account() -> Weight; + fn set_parachain_bond_reserve_percent() -> Weight; + fn set_total_selected() -> Weight; + fn set_collator_commission() -> Weight; + fn set_blocks_per_round() -> Weight; + fn join_candidates(x: u32, ) -> Weight; + fn schedule_leave_candidates(x: u32, ) -> Weight; + fn execute_leave_candidates(x: u32, ) -> Weight; + fn cancel_leave_candidates(x: u32, ) -> Weight; + fn go_offline() -> Weight; + fn go_online() -> Weight; + fn candidate_bond_more() -> Weight; + fn schedule_candidate_bond_less() -> Weight; + fn execute_candidate_bond_less() -> Weight; + fn cancel_candidate_bond_less() -> Weight; + fn delegate(x: u32, y: u32, ) -> Weight; + fn schedule_leave_delegators() -> Weight; + fn execute_leave_delegators(x: u32, ) -> Weight; + fn cancel_leave_delegators() -> Weight; + fn schedule_revoke_delegation() -> Weight; + fn delegator_bond_more() -> Weight; + fn schedule_delegator_bond_less() -> Weight; + fn execute_revoke_delegation() -> Weight; + fn execute_delegator_bond_less() -> Weight; + fn cancel_revoke_delegation() -> Weight; + fn cancel_delegator_bond_less() -> Weight; + fn prepare_staking_payouts() -> Weight; + fn get_rewardable_delegators(y: u32, ) -> Weight; + fn select_top_candidates(x: u32, y: u32, ) -> Weight; + fn pay_one_collator_reward(y: u32, ) -> Weight; + fn base_on_initialize() -> Weight; + fn set_auto_compound(x: u32, y: u32, ) -> Weight; + fn delegate_with_auto_compound(x: u32, y: u32, z: u32, ) -> Weight; + fn mint_collator_reward() -> Weight; +} + +/// Weights for `pallet_parachain_staking` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_staking_expectations() -> Weight { + // Proof Size summary in bytes: + // Measured: `257` + // Estimated: `1742` + // Minimum execution time: 8_700_000 picoseconds. + Weight::from_parts(9_110_000, 1742) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_inflation() -> Weight { + // Proof Size summary in bytes: + // Measured: `293` + // Estimated: `1778` + // Minimum execution time: 33_670_000 picoseconds. + Weight::from_parts(33_970_000, 1778) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::ParachainBondInfo` (r:1 w:1) + /// Proof: `ParachainStaking::ParachainBondInfo` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_parachain_bond_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `225` + // Estimated: `1710` + // Minimum execution time: 9_040_000 picoseconds. + Weight::from_parts(9_290_000, 1710) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::ParachainBondInfo` (r:1 w:1) + /// Proof: `ParachainStaking::ParachainBondInfo` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_parachain_bond_reserve_percent() -> Weight { + // Proof Size summary in bytes: + // Measured: `225` + // Estimated: `1710` + // Minimum execution time: 8_540_000 picoseconds. + Weight::from_parts(8_730_000, 1710) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:1) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_total_selected() -> Weight { + // Proof Size summary in bytes: + // Measured: `205` + // Estimated: `1690` + // Minimum execution time: 8_630_000 picoseconds. + Weight::from_parts(9_040_000, 1690) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::CollatorCommission` (r:1 w:1) + /// Proof: `ParachainStaking::CollatorCommission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_collator_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `196` + // Estimated: `1681` + // Minimum execution time: 7_720_000 picoseconds. + Weight::from_parts(8_120_000, 1681) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::Round` (r:1 w:1) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:0) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_blocks_per_round() -> Weight { + // Proof Size summary in bytes: + // Measured: `293` + // Estimated: `1778` + // Minimum execution time: 34_740_000 picoseconds. + Weight::from_parts(34_960_000, 1778) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:0) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:0 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:0 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn join_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2053 + x * (49 ±0)` + // Estimated: `5289 + x * (50 ±0)` + // Minimum execution time: 49_110_000 picoseconds. + Weight::from_parts(48_800_610, 5289) + // Standard Error: 1_194 + .saturating_add(Weight::from_parts(99_894, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + .saturating_add(Weight::from_parts(0, 50).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn schedule_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1193 + x * (48 ±0)` + // Estimated: `4577 + x * (49 ±0)` + // Minimum execution time: 15_880_000 picoseconds. + Weight::from_parts(10_847_783, 4577) + // Standard Error: 1_150 + .saturating_add(Weight::from_parts(81_054, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:349 w:349) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:350 w:350) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:350 w:350) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[2, 350]`. + fn execute_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `397 + x * (520 ±0)` + // Estimated: `5096 + x * (3424 ±0)` + // Minimum execution time: 95_710_000 picoseconds. + Weight::from_parts(97_460_000, 5096) + // Standard Error: 83_552 + .saturating_add(Weight::from_parts(39_766_801, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(5_u64)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 3424).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn cancel_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1113 + x * (48 ±0)` + // Estimated: `4497 + x * (49 ±0)` + // Minimum execution time: 14_580_000 picoseconds. + Weight::from_parts(9_037_306, 4497) + // Standard Error: 1_135 + .saturating_add(Weight::from_parts(85_841, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn go_offline() -> Weight { + // Proof Size summary in bytes: + // Measured: `499` + // Estimated: `3964` + // Minimum execution time: 13_450_000 picoseconds. + Weight::from_parts(13_950_000, 3964) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn go_online() -> Weight { + // Proof Size summary in bytes: + // Measured: `448` + // Estimated: `3913` + // Minimum execution time: 12_840_000 picoseconds. + Weight::from_parts(13_360_000, 3913) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn candidate_bond_more() -> Weight { + // Proof Size summary in bytes: + // Measured: `660` + // Estimated: `4414` + // Minimum execution time: 43_631_000 picoseconds. + Weight::from_parts(44_710_000, 4414) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `460` + // Estimated: `3925` + // Minimum execution time: 12_480_000 picoseconds. + Weight::from_parts(12_870_000, 3925) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `819` + // Estimated: `4414` + // Minimum execution time: 45_590_000 picoseconds. + Weight::from_parts(46_650_000, 4414) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `444` + // Estimated: `3909` + // Minimum execution time: 10_530_000 picoseconds. + Weight::from_parts(10_770_000, 3909) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 100]`. + /// The range of component `y` is `[2, 300]`. + fn delegate(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2445 + x * (104 ±0) + y * (52 ±0)` + // Estimated: `5622 + x * (106 ±0) + y * (53 ±0)` + // Minimum execution time: 77_740_000 picoseconds. + Weight::from_parts(71_859_523, 5622) + // Standard Error: 1_453 + .saturating_add(Weight::from_parts(104_065, 0).saturating_mul(x.into())) + // Standard Error: 476 + .saturating_add(Weight::from_parts(61_596, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + .saturating_add(Weight::from_parts(0, 106).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 53).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn schedule_leave_delegators() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3960` + // Minimum execution time: 14_980_000 picoseconds. + Weight::from_parts(15_900_000, 3960) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:99 w:99) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:99 w:99) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:99 w:99) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:99 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `x` is `[2, 100]`. + fn execute_leave_delegators(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1037 + x * (463 ±0)` + // Estimated: `4787 + x * (2860 ±0)` + // Minimum execution time: 68_001_000 picoseconds. + Weight::from_parts(5_020_511, 4787) + // Standard Error: 23_750 + .saturating_add(Weight::from_parts(19_600_002, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((4_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2860).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_leave_delegators() -> Weight { + // Proof Size summary in bytes: + // Measured: `620` + // Estimated: `4085` + // Minimum execution time: 17_890_000 picoseconds. + Weight::from_parts(18_520_000, 4085) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3960` + // Minimum execution time: 15_240_000 picoseconds. + Weight::from_parts(15_970_000, 3960) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn delegator_bond_more() -> Weight { + // Proof Size summary in bytes: + // Measured: `1048` + // Estimated: `4513` + // Minimum execution time: 60_430_000 picoseconds. + Weight::from_parts(61_581_000, 4513) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3960` + // Minimum execution time: 15_420_000 picoseconds. + Weight::from_parts(16_120_000, 3960) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1322` + // Estimated: `4787` + // Minimum execution time: 68_370_000 picoseconds. + Weight::from_parts(71_140_000, 4787) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `1276` + // Estimated: `4741` + // Minimum execution time: 62_180_000 picoseconds. + Weight::from_parts(64_180_000, 4741) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `620` + // Estimated: `4085` + // Minimum execution time: 15_570_000 picoseconds. + Weight::from_parts(16_240_000, 4085) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `686` + // Estimated: `4151` + // Minimum execution time: 15_800_000 picoseconds. + Weight::from_parts(16_560_000, 4151) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::Points` (r:1 w:0) + /// Proof: `ParachainStaking::Points` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Staked` (r:1 w:1) + /// Proof: `ParachainStaking::Staked` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:0) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CollatorCommission` (r:1 w:0) + /// Proof: `ParachainStaking::CollatorCommission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelayedPayouts` (r:0 w:1) + /// Proof: `ParachainStaking::DelayedPayouts` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn prepare_staking_payouts() -> Weight { + // Proof Size summary in bytes: + // Measured: `344` + // Estimated: `3809` + // Minimum execution time: 12_010_000 picoseconds. + Weight::from_parts(12_320_000, 3809) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:0) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `y` is `[0, 100]`. + fn get_rewardable_delegators(y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `365 + y * (48 ±0)` + // Estimated: `3829 + y * (48 ±0)` + // Minimum execution time: 7_490_000 picoseconds. + Weight::from_parts(8_230_807, 3829) + // Standard Error: 414 + .saturating_add(Weight::from_parts(39_779, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:0) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:0) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:50 w:0) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:50 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:50 w:0) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:50 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::SelectedCandidates` (r:0 w:1) + /// Proof: `ParachainStaking::SelectedCandidates` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AtStake` (r:0 w:50) + /// Proof: `ParachainStaking::AtStake` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 50]`. + /// The range of component `y` is `[0, 100]`. + fn select_top_candidates(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + x * (5059 ±0) + y * (2400 ±0)` + // Estimated: `8898 + x * (4298 ±54) + y * (852 ±27)` + // Minimum execution time: 8_030_000 picoseconds. + Weight::from_parts(8_410_000, 8898) + // Standard Error: 69_239 + .saturating_add(Weight::from_parts(14_157_366, 0).saturating_mul(x.into())) + // Standard Error: 34_527 + .saturating_add(Weight::from_parts(1_010_819, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((4_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 4298).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 852).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::DelayedPayouts` (r:1 w:0) + /// Proof: `ParachainStaking::DelayedPayouts` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Points` (r:1 w:0) + /// Proof: `ParachainStaking::Points` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AtStake` (r:2 w:1) + /// Proof: `ParachainStaking::AtStake` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AwardedPts` (r:1 w:1) + /// Proof: `ParachainStaking::AwardedPts` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `y` is `[0, 300]`. + fn pay_one_collator_reward(y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `874 + y * (49 ±0)` + // Estimated: `6822 + y * (50 ±0)` + // Minimum execution time: 24_640_000 picoseconds. + Weight::from_parts(42_867_141, 6822) + // Standard Error: 7_625 + .saturating_add(Weight::from_parts(1_583_331, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 50).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn base_on_initialize() -> Weight { + // Proof Size summary in bytes: + // Measured: `347` + // Estimated: `1832` + // Minimum execution time: 3_980_000 picoseconds. + Weight::from_parts(4_110_000, 1832) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:0) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 300]`. + /// The range of component `y` is `[0, 100]`. + fn set_auto_compound(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1006 + x * (34 ±0) + y * (48 ±0)` + // Estimated: `4348 + x * (35 ±0) + y * (49 ±0)` + // Minimum execution time: 21_430_000 picoseconds. + Weight::from_parts(22_249_023, 4348) + // Standard Error: 329 + .saturating_add(Weight::from_parts(35_876, 0).saturating_mul(x.into())) + // Standard Error: 985 + .saturating_add(Weight::from_parts(31_666, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 35).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(y.into())) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 350]`. + /// The range of component `y` is `[0, 350]`. + /// The range of component `z` is `[0, 100]`. + fn delegate_with_auto_compound(x: u32, y: u32, z: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + x * (82 ±0) + y * (33 ±0) + z * (61 ±0)` + // Estimated: `15332 + x * (84 ±0) + y * (44 ±0) + z * (54 ±2)` + // Minimum execution time: 84_020_000 picoseconds. + Weight::from_parts(69_369_914, 15332) + // Standard Error: 792 + .saturating_add(Weight::from_parts(31_047, 0).saturating_mul(x.into())) + // Standard Error: 792 + .saturating_add(Weight::from_parts(35_201, 0).saturating_mul(y.into())) + // Standard Error: 2_766 + .saturating_add(Weight::from_parts(124_801, 0).saturating_mul(z.into())) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + .saturating_add(Weight::from_parts(0, 84).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 44).saturating_mul(y.into())) + .saturating_add(Weight::from_parts(0, 54).saturating_mul(z.into())) + } + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn mint_collator_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `281` + // Estimated: `6196` + // Minimum execution time: 33_490_000 picoseconds. + Weight::from_parts(34_050_000, 6196) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_staking_expectations() -> Weight { + // Proof Size summary in bytes: + // Measured: `257` + // Estimated: `1742` + // Minimum execution time: 8_700_000 picoseconds. + Weight::from_parts(9_110_000, 1742) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_inflation() -> Weight { + // Proof Size summary in bytes: + // Measured: `293` + // Estimated: `1778` + // Minimum execution time: 33_670_000 picoseconds. + Weight::from_parts(33_970_000, 1778) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::ParachainBondInfo` (r:1 w:1) + /// Proof: `ParachainStaking::ParachainBondInfo` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_parachain_bond_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `225` + // Estimated: `1710` + // Minimum execution time: 9_040_000 picoseconds. + Weight::from_parts(9_290_000, 1710) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::ParachainBondInfo` (r:1 w:1) + /// Proof: `ParachainStaking::ParachainBondInfo` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_parachain_bond_reserve_percent() -> Weight { + // Proof Size summary in bytes: + // Measured: `225` + // Estimated: `1710` + // Minimum execution time: 8_540_000 picoseconds. + Weight::from_parts(8_730_000, 1710) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:1) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_total_selected() -> Weight { + // Proof Size summary in bytes: + // Measured: `205` + // Estimated: `1690` + // Minimum execution time: 8_630_000 picoseconds. + Weight::from_parts(9_040_000, 1690) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::CollatorCommission` (r:1 w:1) + /// Proof: `ParachainStaking::CollatorCommission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_collator_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `196` + // Estimated: `1681` + // Minimum execution time: 7_720_000 picoseconds. + Weight::from_parts(8_120_000, 1681) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::Round` (r:1 w:1) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:0) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_blocks_per_round() -> Weight { + // Proof Size summary in bytes: + // Measured: `293` + // Estimated: `1778` + // Minimum execution time: 34_740_000 picoseconds. + Weight::from_parts(34_960_000, 1778) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:0) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:0 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:0 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn join_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2053 + x * (49 ±0)` + // Estimated: `5289 + x * (50 ±0)` + // Minimum execution time: 49_110_000 picoseconds. + Weight::from_parts(48_800_610, 5289) + // Standard Error: 1_194 + .saturating_add(Weight::from_parts(99_894, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + .saturating_add(Weight::from_parts(0, 50).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn schedule_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1193 + x * (48 ±0)` + // Estimated: `4577 + x * (49 ±0)` + // Minimum execution time: 15_880_000 picoseconds. + Weight::from_parts(10_847_783, 4577) + // Standard Error: 1_150 + .saturating_add(Weight::from_parts(81_054, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:349 w:349) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:350 w:350) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:350 w:350) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[2, 350]`. + fn execute_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `397 + x * (520 ±0)` + // Estimated: `5096 + x * (3424 ±0)` + // Minimum execution time: 95_710_000 picoseconds. + Weight::from_parts(97_460_000, 5096) + // Standard Error: 83_552 + .saturating_add(Weight::from_parts(39_766_801, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(x.into()))) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 3424).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn cancel_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1113 + x * (48 ±0)` + // Estimated: `4497 + x * (49 ±0)` + // Minimum execution time: 14_580_000 picoseconds. + Weight::from_parts(9_037_306, 4497) + // Standard Error: 1_135 + .saturating_add(Weight::from_parts(85_841, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn go_offline() -> Weight { + // Proof Size summary in bytes: + // Measured: `499` + // Estimated: `3964` + // Minimum execution time: 13_450_000 picoseconds. + Weight::from_parts(13_950_000, 3964) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn go_online() -> Weight { + // Proof Size summary in bytes: + // Measured: `448` + // Estimated: `3913` + // Minimum execution time: 12_840_000 picoseconds. + Weight::from_parts(13_360_000, 3913) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn candidate_bond_more() -> Weight { + // Proof Size summary in bytes: + // Measured: `660` + // Estimated: `4414` + // Minimum execution time: 43_631_000 picoseconds. + Weight::from_parts(44_710_000, 4414) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `460` + // Estimated: `3925` + // Minimum execution time: 12_480_000 picoseconds. + Weight::from_parts(12_870_000, 3925) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `819` + // Estimated: `4414` + // Minimum execution time: 45_590_000 picoseconds. + Weight::from_parts(46_650_000, 4414) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `444` + // Estimated: `3909` + // Minimum execution time: 10_530_000 picoseconds. + Weight::from_parts(10_770_000, 3909) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 100]`. + /// The range of component `y` is `[2, 300]`. + fn delegate(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2445 + x * (104 ±0) + y * (52 ±0)` + // Estimated: `5622 + x * (106 ±0) + y * (53 ±0)` + // Minimum execution time: 77_740_000 picoseconds. + Weight::from_parts(71_859_523, 5622) + // Standard Error: 1_453 + .saturating_add(Weight::from_parts(104_065, 0).saturating_mul(x.into())) + // Standard Error: 476 + .saturating_add(Weight::from_parts(61_596, 0).saturating_mul(y.into())) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + .saturating_add(Weight::from_parts(0, 106).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 53).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn schedule_leave_delegators() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3960` + // Minimum execution time: 14_980_000 picoseconds. + Weight::from_parts(15_900_000, 3960) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:99 w:99) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:99 w:99) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:99 w:99) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:99 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `x` is `[2, 100]`. + fn execute_leave_delegators(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1037 + x * (463 ±0)` + // Estimated: `4787 + x * (2860 ±0)` + // Minimum execution time: 68_001_000 picoseconds. + Weight::from_parts(5_020_511, 4787) + // Standard Error: 23_750 + .saturating_add(Weight::from_parts(19_600_002, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((4_u64).saturating_mul(x.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2860).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_leave_delegators() -> Weight { + // Proof Size summary in bytes: + // Measured: `620` + // Estimated: `4085` + // Minimum execution time: 17_890_000 picoseconds. + Weight::from_parts(18_520_000, 4085) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3960` + // Minimum execution time: 15_240_000 picoseconds. + Weight::from_parts(15_970_000, 3960) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn delegator_bond_more() -> Weight { + // Proof Size summary in bytes: + // Measured: `1048` + // Estimated: `4513` + // Minimum execution time: 60_430_000 picoseconds. + Weight::from_parts(61_581_000, 4513) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3960` + // Minimum execution time: 15_420_000 picoseconds. + Weight::from_parts(16_120_000, 3960) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1322` + // Estimated: `4787` + // Minimum execution time: 68_370_000 picoseconds. + Weight::from_parts(71_140_000, 4787) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `1276` + // Estimated: `4741` + // Minimum execution time: 62_180_000 picoseconds. + Weight::from_parts(64_180_000, 4741) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `620` + // Estimated: `4085` + // Minimum execution time: 15_570_000 picoseconds. + Weight::from_parts(16_240_000, 4085) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `686` + // Estimated: `4151` + // Minimum execution time: 15_800_000 picoseconds. + Weight::from_parts(16_560_000, 4151) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::Points` (r:1 w:0) + /// Proof: `ParachainStaking::Points` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Staked` (r:1 w:1) + /// Proof: `ParachainStaking::Staked` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:0) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CollatorCommission` (r:1 w:0) + /// Proof: `ParachainStaking::CollatorCommission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelayedPayouts` (r:0 w:1) + /// Proof: `ParachainStaking::DelayedPayouts` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn prepare_staking_payouts() -> Weight { + // Proof Size summary in bytes: + // Measured: `344` + // Estimated: `3809` + // Minimum execution time: 12_010_000 picoseconds. + Weight::from_parts(12_320_000, 3809) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:0) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `y` is `[0, 100]`. + fn get_rewardable_delegators(y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `365 + y * (48 ±0)` + // Estimated: `3829 + y * (48 ±0)` + // Minimum execution time: 7_490_000 picoseconds. + Weight::from_parts(8_230_807, 3829) + // Standard Error: 414 + .saturating_add(Weight::from_parts(39_779, 0).saturating_mul(y.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:0) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:0) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:50 w:0) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:50 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:50 w:0) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:50 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::SelectedCandidates` (r:0 w:1) + /// Proof: `ParachainStaking::SelectedCandidates` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AtStake` (r:0 w:50) + /// Proof: `ParachainStaking::AtStake` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 50]`. + /// The range of component `y` is `[0, 100]`. + fn select_top_candidates(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + x * (5059 ±0) + y * (2400 ±0)` + // Estimated: `8898 + x * (4298 ±54) + y * (852 ±27)` + // Minimum execution time: 8_030_000 picoseconds. + Weight::from_parts(8_410_000, 8898) + // Standard Error: 69_239 + .saturating_add(Weight::from_parts(14_157_366, 0).saturating_mul(x.into())) + // Standard Error: 34_527 + .saturating_add(Weight::from_parts(1_010_819, 0).saturating_mul(y.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((4_u64).saturating_mul(x.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 4298).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 852).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::DelayedPayouts` (r:1 w:0) + /// Proof: `ParachainStaking::DelayedPayouts` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Points` (r:1 w:0) + /// Proof: `ParachainStaking::Points` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AtStake` (r:2 w:1) + /// Proof: `ParachainStaking::AtStake` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AwardedPts` (r:1 w:1) + /// Proof: `ParachainStaking::AwardedPts` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `y` is `[0, 300]`. + fn pay_one_collator_reward(y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `874 + y * (49 ±0)` + // Estimated: `6822 + y * (50 ±0)` + // Minimum execution time: 24_640_000 picoseconds. + Weight::from_parts(42_867_141, 6822) + // Standard Error: 7_625 + .saturating_add(Weight::from_parts(1_583_331, 0).saturating_mul(y.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 50).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn base_on_initialize() -> Weight { + // Proof Size summary in bytes: + // Measured: `347` + // Estimated: `1832` + // Minimum execution time: 3_980_000 picoseconds. + Weight::from_parts(4_110_000, 1832) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:0) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 300]`. + /// The range of component `y` is `[0, 100]`. + fn set_auto_compound(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1006 + x * (34 ±0) + y * (48 ±0)` + // Estimated: `4348 + x * (35 ±0) + y * (49 ±0)` + // Minimum execution time: 21_430_000 picoseconds. + Weight::from_parts(22_249_023, 4348) + // Standard Error: 329 + .saturating_add(Weight::from_parts(35_876, 0).saturating_mul(x.into())) + // Standard Error: 985 + .saturating_add(Weight::from_parts(31_666, 0).saturating_mul(y.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 35).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(y.into())) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(949), added: 3424, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 350]`. + /// The range of component `y` is `[0, 350]`. + /// The range of component `z` is `[0, 100]`. + fn delegate_with_auto_compound(x: u32, y: u32, z: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + x * (82 ±0) + y * (33 ±0) + z * (61 ±0)` + // Estimated: `15332 + x * (84 ±0) + y * (44 ±0) + z * (54 ±2)` + // Minimum execution time: 84_020_000 picoseconds. + Weight::from_parts(69_369_914, 15332) + // Standard Error: 792 + .saturating_add(Weight::from_parts(31_047, 0).saturating_mul(x.into())) + // Standard Error: 792 + .saturating_add(Weight::from_parts(35_201, 0).saturating_mul(y.into())) + // Standard Error: 2_766 + .saturating_add(Weight::from_parts(124_801, 0).saturating_mul(z.into())) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + .saturating_add(Weight::from_parts(0, 84).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 44).saturating_mul(y.into())) + .saturating_add(Weight::from_parts(0, 54).saturating_mul(z.into())) + } + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn mint_collator_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `281` + // Estimated: `6196` + // Minimum execution time: 33_490_000 picoseconds. + Weight::from_parts(34_050_000, 6196) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } +} \ No newline at end of file From 0cf46662b6506bc23edaad276cbd9e01c54d05a7 Mon Sep 17 00:00:00 2001 From: NZT48 Date: Sun, 22 Dec 2024 22:05:22 +0100 Subject: [PATCH 3/6] Ignore deprecation notice for run command --- node/src/command.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/command.rs b/node/src/command.rs index 2dbde9b..267b750 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -188,6 +188,7 @@ pub fn run() -> Result<()> { match cmd { BenchmarkCmd::Pallet(cmd) => { if cfg!(feature = "runtime-benchmarks") { + #[allow(deprecated)] runner.sync_run(|config| cmd.run::, ()>(config)) } else { Err("Benchmarking wasn't enabled when building the node. \ From 2c8247e0e68e3bd4c47e72a4123c57714f16fe2a Mon Sep 17 00:00:00 2001 From: NZT48 Date: Sun, 22 Dec 2024 22:05:45 +0100 Subject: [PATCH 4/6] Add parachain staking weights --- .../src/weights/pallet_parachain_staking.rs | 748 ++++++++++++++++++ 1 file changed, 748 insertions(+) create mode 100644 runtime/src/weights/pallet_parachain_staking.rs diff --git a/runtime/src/weights/pallet_parachain_staking.rs b/runtime/src/weights/pallet_parachain_staking.rs new file mode 100644 index 0000000..997fc63 --- /dev/null +++ b/runtime/src/weights/pallet_parachain_staking.rs @@ -0,0 +1,748 @@ + +//! Autogenerated weights for `pallet_parachain_staking` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 39.0.0 +//! DATE: 2024-08-30, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `ip-172-31-23-147`, CPU: `AMD EPYC 9R14` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("neuroweb-paseo-local")`, DB CACHE: 1024 + +// Executed Command: +// ./target/release/neuroweb +// benchmark +// pallet +// --wasm-execution=compiled +// --pallet=pallet_parachain_staking +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=./runtimes/neuroweb/src/weights/pallet_parachain_staking.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_parachain_staking`. +pub struct WeightInfo(PhantomData); +impl pallet_parachain_staking::WeightInfo for WeightInfo { + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_staking_expectations() -> Weight { + // Proof Size summary in bytes: + // Measured: `324` + // Estimated: `1809` + // Minimum execution time: 8_080_000 picoseconds. + Weight::from_parts(8_420_000, 0) + .saturating_add(Weight::from_parts(0, 1809)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_inflation() -> Weight { + // Proof Size summary in bytes: + // Measured: `360` + // Estimated: `1845` + // Minimum execution time: 32_520_000 picoseconds. + Weight::from_parts(32_950_000, 0) + .saturating_add(Weight::from_parts(0, 1845)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::ParachainBondInfo` (r:1 w:1) + /// Proof: `ParachainStaking::ParachainBondInfo` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_parachain_bond_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `1777` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(8_370_000, 0) + .saturating_add(Weight::from_parts(0, 1777)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::ParachainBondInfo` (r:1 w:1) + /// Proof: `ParachainStaking::ParachainBondInfo` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_parachain_bond_reserve_percent() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `1777` + // Minimum execution time: 7_990_000 picoseconds. + Weight::from_parts(8_230_000, 0) + .saturating_add(Weight::from_parts(0, 1777)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:1) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_total_selected() -> Weight { + // Proof Size summary in bytes: + // Measured: `272` + // Estimated: `1757` + // Minimum execution time: 8_010_000 picoseconds. + Weight::from_parts(8_410_000, 0) + .saturating_add(Weight::from_parts(0, 1757)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::CollatorCommission` (r:1 w:1) + /// Proof: `ParachainStaking::CollatorCommission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_collator_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `263` + // Estimated: `1748` + // Minimum execution time: 7_130_000 picoseconds. + Weight::from_parts(7_470_000, 0) + .saturating_add(Weight::from_parts(0, 1748)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::Round` (r:1 w:1) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:0) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:1) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_blocks_per_round() -> Weight { + // Proof Size summary in bytes: + // Measured: `360` + // Estimated: `1845` + // Minimum execution time: 33_521_000 picoseconds. + Weight::from_parts(33_840_000, 0) + .saturating_add(Weight::from_parts(0, 1845)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:0) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:0 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:0 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn join_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2120 + x * (49 ±0)` + // Estimated: `5356 + x * (50 ±0)` + // Minimum execution time: 49_340_000 picoseconds. + Weight::from_parts(48_874_921, 0) + .saturating_add(Weight::from_parts(0, 5356)) + // Standard Error: 1_260 + .saturating_add(Weight::from_parts(95_958, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(7)) + .saturating_add(Weight::from_parts(0, 50).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn schedule_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1260 + x * (48 ±0)` + // Estimated: `4644 + x * (49 ±0)` + // Minimum execution time: 15_360_000 picoseconds. + Weight::from_parts(9_155_653, 0) + .saturating_add(Weight::from_parts(0, 4644)) + // Standard Error: 1_277 + .saturating_add(Weight::from_parts(80_365, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:349 w:349) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:350 w:350) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:350 w:350) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[2, 350]`. + fn execute_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `468 + x * (520 ±0)` + // Estimated: `5163 + x * (2969 ±0)` + // Minimum execution time: 98_191_000 picoseconds. + Weight::from_parts(98_990_000, 0) + .saturating_add(Weight::from_parts(0, 5163)) + // Standard Error: 81_946 + .saturating_add(Weight::from_parts(41_889_065, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(5)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2969).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 1000]`. + fn cancel_leave_candidates(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1180 + x * (48 ±0)` + // Estimated: `4564 + x * (49 ±0)` + // Minimum execution time: 13_810_000 picoseconds. + Weight::from_parts(6_943_588, 0) + .saturating_add(Weight::from_parts(0, 4564)) + // Standard Error: 1_314 + .saturating_add(Weight::from_parts(86_432, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn go_offline() -> Weight { + // Proof Size summary in bytes: + // Measured: `566` + // Estimated: `4031` + // Minimum execution time: 12_680_000 picoseconds. + Weight::from_parts(13_250_000, 0) + .saturating_add(Weight::from_parts(0, 4031)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn go_online() -> Weight { + // Proof Size summary in bytes: + // Measured: `515` + // Estimated: `3980` + // Minimum execution time: 12_180_000 picoseconds. + Weight::from_parts(12_890_000, 0) + .saturating_add(Weight::from_parts(0, 3980)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn candidate_bond_more() -> Weight { + // Proof Size summary in bytes: + // Measured: `764` + // Estimated: `4229` + // Minimum execution time: 46_141_000 picoseconds. + Weight::from_parts(46_880_000, 0) + .saturating_add(Weight::from_parts(0, 4229)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(5)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `527` + // Estimated: `3992` + // Minimum execution time: 11_970_000 picoseconds. + Weight::from_parts(12_390_000, 0) + .saturating_add(Weight::from_parts(0, 3992)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `923` + // Estimated: `4388` + // Minimum execution time: 47_250_000 picoseconds. + Weight::from_parts(48_491_000, 0) + .saturating_add(Weight::from_parts(0, 4388)) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(5)) + } + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_candidate_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `511` + // Estimated: `3976` + // Minimum execution time: 9_960_000 picoseconds. + Weight::from_parts(10_370_000, 0) + .saturating_add(Weight::from_parts(0, 3976)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[3, 100]`. + /// The range of component `y` is `[2, 300]`. + fn delegate(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2512 + x * (104 ±0) + y * (52 ±0)` + // Estimated: `5689 + x * (106 ±0) + y * (53 ±0)` + // Minimum execution time: 79_901_000 picoseconds. + Weight::from_parts(73_660_410, 0) + .saturating_add(Weight::from_parts(0, 5689)) + // Standard Error: 1_418 + .saturating_add(Weight::from_parts(96_002, 0).saturating_mul(x.into())) + // Standard Error: 465 + .saturating_add(Weight::from_parts(58_455, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(7)) + .saturating_add(Weight::from_parts(0, 106).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 53).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn schedule_leave_delegators() -> Weight { + // Proof Size summary in bytes: + // Measured: `562` + // Estimated: `4027` + // Minimum execution time: 15_130_000 picoseconds. + Weight::from_parts(15_851_000, 0) + .saturating_add(Weight::from_parts(0, 4027)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:99 w:99) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:99 w:99) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:99 w:99) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:99 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `x` is `[2, 100]`. + fn execute_leave_delegators(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1104 + x * (463 ±0)` + // Estimated: `4854 + x * (2860 ±2)` + // Minimum execution time: 67_310_000 picoseconds. + Weight::from_parts(4_621_959, 0) + .saturating_add(Weight::from_parts(0, 4854)) + // Standard Error: 24_365 + .saturating_add(Weight::from_parts(18_846_066, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((4_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2860).saturating_mul(x.into())) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_leave_delegators() -> Weight { + // Proof Size summary in bytes: + // Measured: `687` + // Estimated: `4152` + // Minimum execution time: 16_680_000 picoseconds. + Weight::from_parts(17_250_000, 0) + .saturating_add(Weight::from_parts(0, 4152)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `562` + // Estimated: `4027` + // Minimum execution time: 14_920_000 picoseconds. + Weight::from_parts(15_380_000, 0) + .saturating_add(Weight::from_parts(0, 4027)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn delegator_bond_more() -> Weight { + // Proof Size summary in bytes: + // Measured: `1115` + // Estimated: `4580` + // Minimum execution time: 62_931_000 picoseconds. + Weight::from_parts(64_050_000, 0) + .saturating_add(Weight::from_parts(0, 4580)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(7)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn schedule_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `562` + // Estimated: `4027` + // Minimum execution time: 14_720_000 picoseconds. + Weight::from_parts(15_270_000, 0) + .saturating_add(Weight::from_parts(0, 4027)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1389` + // Estimated: `4854` + // Minimum execution time: 67_571_000 picoseconds. + Weight::from_parts(69_880_000, 0) + .saturating_add(Weight::from_parts(0, 4854)) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().writes(8)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn execute_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `1343` + // Estimated: `4808` + // Minimum execution time: 64_570_000 picoseconds. + Weight::from_parts(66_250_000, 0) + .saturating_add(Weight::from_parts(0, 4808)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(8)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_revoke_delegation() -> Weight { + // Proof Size summary in bytes: + // Measured: `687` + // Estimated: `4152` + // Minimum execution time: 14_920_000 picoseconds. + Weight::from_parts(15_630_000, 0) + .saturating_add(Weight::from_parts(0, 4152)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:1) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn cancel_delegator_bond_less() -> Weight { + // Proof Size summary in bytes: + // Measured: `753` + // Estimated: `4218` + // Minimum execution time: 15_120_000 picoseconds. + Weight::from_parts(15_810_000, 0) + .saturating_add(Weight::from_parts(0, 4218)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::Points` (r:1 w:0) + /// Proof: `ParachainStaking::Points` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Staked` (r:1 w:1) + /// Proof: `ParachainStaking::Staked` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::InflationConfig` (r:1 w:0) + /// Proof: `ParachainStaking::InflationConfig` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CollatorCommission` (r:1 w:0) + /// Proof: `ParachainStaking::CollatorCommission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelayedPayouts` (r:0 w:1) + /// Proof: `ParachainStaking::DelayedPayouts` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn prepare_staking_payouts() -> Weight { + // Proof Size summary in bytes: + // Measured: `411` + // Estimated: `3876` + // Minimum execution time: 11_530_000 picoseconds. + Weight::from_parts(11_891_000, 0) + .saturating_add(Weight::from_parts(0, 3876)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:1 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:0) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `y` is `[0, 100]`. + fn get_rewardable_delegators(y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `432 + y * (48 ±0)` + // Estimated: `3896 + y * (48 ±0)` + // Minimum execution time: 7_460_000 picoseconds. + Weight::from_parts(8_069_065, 0) + .saturating_add(Weight::from_parts(0, 3896)) + // Standard Error: 338 + .saturating_add(Weight::from_parts(36_811, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::TotalSelected` (r:1 w:0) + /// Proof: `ParachainStaking::TotalSelected` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:0) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:50 w:0) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::DelegationScheduledRequests` (r:50 w:0) + /// Proof: `ParachainStaking::DelegationScheduledRequests` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:50 w:0) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:50 w:0) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::SelectedCandidates` (r:0 w:1) + /// Proof: `ParachainStaking::SelectedCandidates` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AtStake` (r:0 w:50) + /// Proof: `ParachainStaking::AtStake` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 50]`. + /// The range of component `y` is `[0, 100]`. + fn select_top_candidates(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + x * (5059 ±0) + y * (2400 ±0)` + // Estimated: `8965 + x * (4298 ±54) + y * (852 ±27)` + // Minimum execution time: 7_990_000 picoseconds. + Weight::from_parts(8_200_000, 0) + .saturating_add(Weight::from_parts(0, 8965)) + // Standard Error: 70_579 + .saturating_add(Weight::from_parts(13_593_953, 0).saturating_mul(x.into())) + // Standard Error: 35_195 + .saturating_add(Weight::from_parts(1_000_799, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((4_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 4298).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 852).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::DelayedPayouts` (r:1 w:0) + /// Proof: `ParachainStaking::DelayedPayouts` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::Points` (r:1 w:0) + /// Proof: `ParachainStaking::Points` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AtStake` (r:2 w:1) + /// Proof: `ParachainStaking::AtStake` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AwardedPts` (r:1 w:1) + /// Proof: `ParachainStaking::AwardedPts` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `y` is `[0, 300]`. + fn pay_one_collator_reward(y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `941 + y * (49 ±0)` + // Estimated: `6886 + y * (50 ±0)` + // Minimum execution time: 24_530_000 picoseconds. + Weight::from_parts(43_157_478, 0) + .saturating_add(Weight::from_parts(0, 6886)) + // Standard Error: 7_767 + .saturating_add(Weight::from_parts(1_545_269, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 50).saturating_mul(y.into())) + } + /// Storage: `ParachainStaking::Round` (r:1 w:0) + /// Proof: `ParachainStaking::Round` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn base_on_initialize() -> Weight { + // Proof Size summary in bytes: + // Measured: `414` + // Estimated: `1899` + // Minimum execution time: 3_950_000 picoseconds. + Weight::from_parts(4_100_000, 0) + .saturating_add(Weight::from_parts(0, 1899)) + .saturating_add(T::DbWeight::get().reads(1)) + } + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:0) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 300]`. + /// The range of component `y` is `[0, 100]`. + fn set_auto_compound(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1073 + x * (34 ±0) + y * (48 ±0)` + // Estimated: `4415 + x * (35 ±0) + y * (49 ±0)` + // Minimum execution time: 20_950_000 picoseconds. + Weight::from_parts(21_814_717, 0) + .saturating_add(Weight::from_parts(0, 4415)) + // Standard Error: 260 + .saturating_add(Weight::from_parts(30_633, 0).saturating_mul(x.into())) + // Standard Error: 780 + .saturating_add(Weight::from_parts(21_130, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(Weight::from_parts(0, 35).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(y.into())) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::DelegatorState` (r:1 w:1) + /// Proof: `ParachainStaking::DelegatorState` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidateInfo` (r:1 w:1) + /// Proof: `ParachainStaking::CandidateInfo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::AutoCompoundingDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::AutoCompoundingDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::BottomDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::BottomDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) + /// Storage: `ParachainStaking::Total` (r:1 w:1) + /// Proof: `ParachainStaking::Total` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::TopDelegations` (r:1 w:1) + /// Proof: `ParachainStaking::TopDelegations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ParachainStaking::CandidatePool` (r:1 w:1) + /// Proof: `ParachainStaking::CandidatePool` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `x` is `[0, 350]`. + /// The range of component `y` is `[0, 350]`. + /// The range of component `z` is `[0, 100]`. + fn delegate_with_auto_compound(x: u32, y: u32, z: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + x * (82 ±0) + y * (33 ±0) + z * (61 ±0)` + // Estimated: `15399 + x * (84 ±0) + y * (44 ±0) + z * (54 ±2)` + // Minimum execution time: 85_200_000 picoseconds. + Weight::from_parts(73_017_192, 0) + .saturating_add(Weight::from_parts(0, 15399)) + // Standard Error: 729 + .saturating_add(Weight::from_parts(22_980, 0).saturating_mul(x.into())) + // Standard Error: 729 + .saturating_add(Weight::from_parts(30_661, 0).saturating_mul(y.into())) + // Standard Error: 2_545 + .saturating_add(Weight::from_parts(106_185, 0).saturating_mul(z.into())) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(Weight::from_parts(0, 84).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 44).saturating_mul(y.into())) + .saturating_add(Weight::from_parts(0, 54).saturating_mul(z.into())) + } + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn mint_collator_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `318` + // Estimated: `6196` + // Minimum execution time: 32_410_000 picoseconds. + Weight::from_parts(33_530_000, 0) + .saturating_add(Weight::from_parts(0, 6196)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } +} From e5ea1b617a5bfa67f104911db6d41d3790dfc75f Mon Sep 17 00:00:00 2001 From: NZT48 Date: Sun, 22 Dec 2024 22:06:31 +0100 Subject: [PATCH 5/6] Integrate parachain staking into runtime --- precompiles/assets-erc20/src/mock.rs | 15 +++- runtime/Cargo.toml | 5 ++ runtime/src/lib.rs | 115 +++++++++++++++++++-------- runtime/src/weights/mod.rs | 1 + 4 files changed, 98 insertions(+), 38 deletions(-) diff --git a/precompiles/assets-erc20/src/mock.rs b/precompiles/assets-erc20/src/mock.rs index c041729..8c0302a 100644 --- a/precompiles/assets-erc20/src/mock.rs +++ b/precompiles/assets-erc20/src/mock.rs @@ -149,14 +149,14 @@ impl frame_system::Config for Runtime { type BaseCallFilter = Everything; type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type BlockNumber = BlockNumber; + // type Index = u64; + // type BlockNumber = BlockNumber; type RuntimeCall = RuntimeCall; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; - type Header = Header; + // type Header = Header; type RuntimeEvent = RuntimeEvent; type BlockHashCount = BlockHashCount; type Version = (); @@ -170,6 +170,7 @@ impl frame_system::Config for Runtime { type SS58Prefix = SS58Prefix; type OnSetCode = (); type MaxConsumers = frame_support::traits::ConstU32<16>; + type RuntimeTask = RuntimeTask; } parameter_types! { @@ -197,6 +198,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type MaxFreezes = (); } parameter_types! { @@ -204,6 +209,7 @@ parameter_types! { Erc20AssetsPrecompileSet(PhantomData); pub const WeightPerGas: Weight = Weight::from_parts(1, 0); pub BlockGasLimit: U256 = U256::max_value(); + pub const GasLimitPovSizeRatio: u64 = 4; } impl pallet_evm::Config for Runtime { @@ -226,6 +232,9 @@ impl pallet_evm::Config for Runtime { type FindAuthor = (); type OnCreate = (); type WeightInfo = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + } // These parameters dont matter much as this will only be called by root with the forced arguments diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d1f8144..a280edc 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -98,6 +98,9 @@ pallet-democracy = { workspace = true } pallet-identity = { workspace = true } pallet-preimage = { workspace = true } +# Staking +pallet-parachain-staking = { workspace = true } + [build-dependencies] substrate-wasm-builder = { workspace = true, optional = true } @@ -137,6 +140,7 @@ std = [ "pallet-evm-precompile-sha3fips/std", "pallet-ethereum/std", "pallet-identity/std", + "pallet-parachain-staking/std", "pallet-preimage/std", "pallet-proxy/std", "pallet-message-queue/std", @@ -189,6 +193,7 @@ runtime-benchmarks = [ "pallet-identity/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", + "pallet-parachain-staking/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 45c002d..f0202ea 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -8,6 +8,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); mod weights; pub mod xcm_config; +pub use frame_support::traits::Get; use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; use smallvec::smallvec; @@ -25,7 +26,7 @@ use sp_runtime::{ }, ApplyExtrinsicResult, MultiSignature, RuntimeDebug, }; - +use sp_runtime::Percent; use sp_std::prelude::*; #[cfg(feature = "std")] use sp_version::NativeVersion; @@ -33,6 +34,7 @@ use sp_version::RuntimeVersion; use codec::{Encode, Decode, MaxEncodedLen}; use frame_support::{ + ord_parameter_types, construct_runtime, parameter_types, transactional, genesis_builder_helper::{build_state, get_preset}, traits::{ @@ -58,7 +60,7 @@ use frame_system::{ pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; pub use sp_runtime::{MultiAddress, Perbill, Permill}; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; -use xcm_config::{XcmOriginToTransactDispatchOrigin}; +use xcm_config::XcmOriginToTransactDispatchOrigin; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; @@ -68,6 +70,7 @@ use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; use polkadot_runtime_common::xcm_sender::NoPriceForMessageDelivery; +pub use pallet_parachain_staking; // XCM Imports use xcm::latest::prelude::BodyId; @@ -319,7 +322,7 @@ impl pallet_timestamp::Config for Runtime { impl pallet_authorship::Config for Runtime { type FindAuthor = pallet_session::FindAccountFromAuthorIndex; - type EventHandler = (CollatorSelection,); + type EventHandler = ParachainStaking; } parameter_types! { @@ -344,11 +347,11 @@ impl pallet_balances::Config for Runtime { type MaxFreezes = ConstU32<1>; } -pub struct ToStakingPot; -impl OnUnbalanced> for ToStakingPot +pub struct CollatorsIncentivesPot; +impl OnUnbalanced> for CollatorsIncentivesPot { fn on_nonzero_unbalanced(amount: Credit) { - let staking_pot = PotId::get().into_account_truncating(); + let staking_pot = CollatorsIncentivesPalletId::get().into_account_truncating(); let _ = Balances::resolve(&staking_pot, amount); } } @@ -400,7 +403,7 @@ impl OnUnbalanced> for DealWithFees let (future_auctions_fees, treasury_fees) = split.1.ration(75, 25); >::on_unbalanced(treasury_fees); - >::on_unbalanced(collators_incentives_fees); + >::on_unbalanced(collators_incentives_fees); >::on_unbalanced(future_auctions_fees); >::on_unbalanced(dkg_incentives_fees); } @@ -415,7 +418,7 @@ impl OnUnbalanced> for DealWithFees >::on_unbalanced(treasury_fees); - >::on_unbalanced(collators_incentives_fees); + >::on_unbalanced(collators_incentives_fees); >::on_unbalanced(future_auctions_fees); >::on_unbalanced(dkg_incentives_fees); } @@ -549,11 +552,10 @@ parameter_types! { impl pallet_session::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorId = ::AccountId; - // we don't have stash and controller, thus we don't need the convert as well. - type ValidatorIdOf = pallet_collator_selection::IdentityCollator; - type ShouldEndSession = pallet_session::PeriodicSessions; - type NextSessionRotation = pallet_session::PeriodicSessions; - type SessionManager = CollatorSelection; + type ValidatorIdOf = ConvertInto; + type ShouldEndSession = ParachainStaking; + type NextSessionRotation = ParachainStaking; + type SessionManager = ParachainStaking; // Essentially just Aura, but lets be pedantic. type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; @@ -578,25 +580,6 @@ parameter_types! { pub const ExecutiveBody: BodyId = BodyId::Executive; } -// We allow root only to execute privileged collator selection operations. -pub type CollatorSelectionUpdateOrigin = EnsureRoot; - -impl pallet_collator_selection::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type UpdateOrigin = CollatorSelectionUpdateOrigin; - type PotId = PotId; - type MaxCandidates = MaxCandidates; - type MinEligibleCollators = MinEligibleCollators; - type MaxInvulnerables = MaxInvulnerables; - // should be a multiple of session or things will get inconsistent - type KickThreshold = Period; - type ValidatorId = ::AccountId; - type ValidatorIdOf = pallet_collator_selection::IdentityCollator; - type ValidatorRegistration = Session; - type WeightInfo = (); -} - // Define the types required by the Scheduler pallet. parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * MAXIMUM_BLOCK_WEIGHT; @@ -1137,6 +1120,67 @@ impl pallet_proxy::Config for Runtime { type AnnouncementDepositFactor = AnnouncementDepositFactor; } +parameter_types! { + /// Default fixed percent a collator takes off the top of due rewards + pub const DefaultCollatorCommission: Perbill = Perbill::from_percent(33); + /// Default percent of inflation set aside for parachain bond every round + pub const DefaultParachainBondReservePercent: Percent = Percent::from_percent(0); + pub const MinDelegation: Balance = 100 * OTP; + pub const MinDelegatorStk: Balance = 100 * OTP; + pub const MinCandidateStk: Balance = 100000 * OTP; +} + +ord_parameter_types! { + pub const StakingPot: AccountId = + AccountIdConversion::::into_account_truncating(&CollatorsIncentivesPalletId::get()); +} + + +impl pallet_parachain_staking::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type PayMaster = StakingPot; + type Balance = Balance; + type Currency = Balances; + type MonetaryGovernanceOrigin = EnsureRootOrFourFiftsOfCouncil; + /// Minimum round length is 2 minutes (10 * 6 second block times) + type MinBlocksPerRound = ConstU32<{3 * HOURS}>; + /// Rounds before the collator leaving the candidates request can be executed + type LeaveCandidatesDelay = ConstU32<{ 4 * 7 }>; + /// Rounds before the candidate bond increase/decrease can be executed + type CandidateBondLessDelay = ConstU32<{ 4 * 7 }>; + /// Rounds before the delegator exit can be executed + type LeaveDelegatorsDelay = ConstU32<{ 4 * 7 }>; + /// Rounds before the delegator revocation can be executed + type RevokeDelegationDelay = ConstU32<{ 4 * 7 }>; + /// Rounds before the delegator bond increase/decrease can be executed + type DelegationBondLessDelay = ConstU32<{ 4 * 7 }>; + /// Rounds before the reward is paid + type RewardPaymentDelay = ConstU32<2>; + /// Minimum collators selected per round, default at genesis and minimum forever after + /// TODO: Incerease this after release + type MinSelectedCandidates = ConstU32<1>; + /// Maximum top delegations per candidate + type MaxTopDelegationsPerCandidate = ConstU32<300>; + /// Maximum bottom delegations per candidate + type MaxBottomDelegationsPerCandidate = ConstU32<50>; + /// Maximum delegations per delegator + type MaxDelegationsPerDelegator = ConstU32<100>; + /// Minimum stake required to be reserved to be a candidate + type MinCandidateStk = MinCandidateStk; + /// Minimum stake for any registered on-chain account to delegate + type MinDelegation = MinDelegation; + /// Minimum stake for any registered on-chain account to be a delegator + type MinDelegatorStk = MinDelegatorStk; + // We use the default implementation, so we leave () here. + type OnCollatorPayout = (); + // We use the default implementation, so we leave () here. + type PayoutCollatorReward = (); + // We use the default implementation, so we leave () here. + type OnNewRound = (); + type WeightInfo = weights::pallet_parachain_staking::WeightInfo; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( @@ -1163,10 +1207,12 @@ construct_runtime!( // Collator support. The order of these 4 are important and shall not change. Authorship: pallet_authorship::{Pallet, Storage} = 20, - CollatorSelection: pallet_collator_selection::{Pallet, Call, Storage, Event, Config} = 21, + // Disebled after implementation of Staking + // CollatorSelection: pallet_collator_selection::{Pallet, Call, Storage, Event, Config} = 21, Session: pallet_session::{Pallet, Call, Storage, Event, Config} = 22, Aura: pallet_aura::{Pallet, Storage, Config} = 23, AuraExt: cumulus_pallet_aura_ext::{Pallet, Storage, Config} = 24, + ParachainStaking: pallet_parachain_staking = 25, // XCM helpers. XcmpQueue: cumulus_pallet_xcmp_queue::{Pallet, Call, Storage, Event} = 30, @@ -1187,7 +1233,6 @@ construct_runtime!( Council: pallet_collective:: = 62, Democracy: pallet_democracy = 63, Identity: pallet_identity = 64, - } ); @@ -1206,7 +1251,7 @@ mod benches { [pallet_preimage, Preimage] [pallet_proxy, Proxy] [pallet_timestamp, Timestamp] - [pallet_collator_selection, CollatorSelection] + [pallet_parachain_staking, ParachainStaking] [cumulus_pallet_xcmp_queue, XcmpQueue] [pallet_utility, Utility] ); diff --git a/runtime/src/weights/mod.rs b/runtime/src/weights/mod.rs index bd1e58d..9531793 100644 --- a/runtime/src/weights/mod.rs +++ b/runtime/src/weights/mod.rs @@ -24,6 +24,7 @@ pub mod rocksdb_weights; pub mod evm_accounts_weights; pub mod pallet_xc_asset_config; pub mod pallet_xcm; +pub mod pallet_parachain_staking; pub use block_weights::constants::BlockExecutionWeight; pub use extrinsic_weights::constants::ExtrinsicBaseWeight; From 5beb40c840d284ca1bc80ee49e0f958366879f96 Mon Sep 17 00:00:00 2001 From: NZT48 Date: Sun, 22 Dec 2024 22:07:15 +0100 Subject: [PATCH 6/6] Setup parachain staking in chain spec generation --- Cargo.lock | 49 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 +++++++++++ node/Cargo.toml | 1 + node/src/chain_spec.rs | 51 +++++++++++++++++++++++++++++++++--------- 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16efcd9..cd0c231 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6653,6 +6653,7 @@ dependencies = [ "frame-benchmarking 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", "frame-benchmarking-cli 32.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", "futures", + "itertools 0.11.0", "jsonrpsee", "log", "neuroweb-runtime", @@ -6743,6 +6744,7 @@ dependencies = [ "pallet-identity", "pallet-message-queue 31.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", "pallet-multisig", + "pallet-parachain-staking", "pallet-preimage", "pallet-proxy", "pallet-scheduler", @@ -8111,6 +8113,32 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", ] +[[package]] +name = "pallet-parachain-staking" +version = "1.0.0" +dependencies = [ + "frame-benchmarking 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "frame-support 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "frame-system 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "log", + "pallet-aura", + "pallet-authorship 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "pallet-balances 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "pallet-session 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "pallet-timestamp 27.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "parity-scale-codec", + "scale-info", + "serde", + "similar-asserts", + "sp-consensus-aura", + "sp-core 28.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "sp-io 30.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "sp-runtime 31.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "sp-staking 26.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", + "substrate-fixed", +] + [[package]] name = "pallet-parameters" version = "0.1.0" @@ -17400,6 +17428,17 @@ name = "substrate-build-script-utils" version = "11.0.0" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0#8c8edacf8942298c3807a2e192860da9e7e4996a" +[[package]] +name = "substrate-fixed" +version = "0.5.9" +source = "git+https://github.com/encointer/substrate-fixed#879c58bcc6fd676a74315dcd38b598f28708b0b5" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "substrate-typenum", +] + [[package]] name = "substrate-frame-rpc-system" version = "28.0.0" @@ -17496,6 +17535,16 @@ dependencies = [ "trie-db", ] +[[package]] +name = "substrate-typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f0091e93c2c75b233ae39424c52cb8a662c0811fb68add149e20e5d7e8a788" +dependencies = [ + "parity-scale-codec", + "scale-info", +] + [[package]] name = "substrate-wasm-builder" version = "17.0.0" diff --git a/Cargo.toml b/Cargo.toml index 6dbb8ab..f001abb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,10 @@ slices = "0.2.0" smallvec = "1.10.0" serde = { version = "1.0.152", default-features = false } serde_json = "1.0.92" +itertools = { version = "0.11", default-features = false, features = [ + "use_alloc", +] } +rustc-hex = { version = "2.0.1", default-features = false } # Substrate frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } @@ -80,9 +84,11 @@ sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk", defau sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.11.0", default-features = false } sp-keystore = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.11.0" } +sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.11.0", default-features = false } sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } sp-session = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } +sp-staking = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } sp-std = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.11.0" } sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } @@ -177,6 +183,13 @@ xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-s xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } +# Staking +substrate-fixed = { git = "https://github.com/encointer/substrate-fixed", default-features = false } +pallet-parachain-staking = { path = "pallets/parachain-staking", default-features = false } +#pallet-evm-precompile-parachain-staking = { path = "precompiles/parachain-staking", default-features = false } +# num-integer = { version = "0.1", default-features = false } +# sp-consensus-slots = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.11.0" } + # Local neuroweb-runtime = { path = "./runtime" } precompile-utils = { path = "./precompiles/utils", default-features = false } diff --git a/node/Cargo.toml b/node/Cargo.toml index bc20ed6..bc5f722 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -21,6 +21,7 @@ codec = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["arbitrary_precision"] } jsonrpsee = { workspace = true } +itertools = { workspace = true } # Local neuroweb-runtime = { workspace = true } diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index b594936..fdb9bf6 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -1,19 +1,46 @@ use cumulus_primitives_core::ParaId; -use neuroweb_runtime::{AccountId, AuraId, - EVMConfig, Signature, EXISTENTIAL_DEPOSIT}; +use neuroweb_runtime::{pallet_parachain_staking::{ + inflation::{perbill_annual_to_perbill_round, BLOCKS_PER_YEAR}, + InflationInfo, Range, + }, AccountId, AuraId, Balance, EVMConfig, MinCandidateStk, Signature, OTP}; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; use serde::{Deserialize, Serialize}; use sp_core::{sr25519, Pair, Public, H160, U256}; -use sp_runtime::traits::{IdentifyAccount, Verify}; +use sp_runtime::{Perbill, Percent, traits::{IdentifyAccount, Verify}}; use std::{collections::BTreeMap, str::FromStr}; +use itertools::Itertools; /// Specialized `ChainSpec` for the normal parachain runtime. pub type ChainSpec = sc_service::GenericChainSpec; /// The default XCM version to set in genesis config. -const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; +pub const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; +pub const COLLATOR_COMMISSION: Perbill = Perbill::from_percent(10); +pub const PARACHAIN_BOND_RESERVE_PERCENT: Percent = Percent::from_percent(0); +pub const BLOCKS_PER_ROUND: u32 = 3600; // 6 hours of blocks +pub const NUM_SELECTED_CANDIDATES: u32 = 1; // For start + +pub fn neuroweb_inflation_config() -> InflationInfo { + fn to_round_inflation(annual: Range) -> Range { + perbill_annual_to_perbill_round( + annual, + // rounds per year + BLOCKS_PER_YEAR / BLOCKS_PER_ROUND, + ) + } + let annual = + Range { min: Perbill::from_percent(2), ideal: Perbill::from_percent(3), max: Perbill::from_percent(3) }; + + InflationInfo { + // staking expectations + expect: Range { min: 100_000 * OTP, ideal: 200_000 * OTP, max: 500_000 * OTP }, + // annual inflation + annual, + round: to_round_inflation(annual), + } +} /// Helper function to generate a crypto pair from seed pub fn get_from_seed(seed: &str) -> ::Public { @@ -174,13 +201,8 @@ fn testnet_genesis( parachain_id: id, ..Default::default() }, - collator_selection: neuroweb_runtime::CollatorSelectionConfig { - invulnerables: invulnerables.iter().cloned().map(|(acc, _)| acc).collect(), - candidacy_bond: EXISTENTIAL_DEPOSIT * 16, - ..Default::default() - }, session: neuroweb_runtime::SessionConfig { - keys: invulnerables + keys: invulnerables.clone() .into_iter() .map(|(acc, aura)| { ( @@ -242,6 +264,15 @@ fn testnet_genesis( council: Default::default(), democracy: Default::default(), transaction_payment: Default::default(), + parachain_staking: neuroweb_runtime::ParachainStakingConfig { + candidates: invulnerables.clone().into_iter().map(|account| (account.0, MinCandidateStk::get())).collect_vec(), + delegations: vec![], + blocks_per_round: BLOCKS_PER_ROUND, + num_selected_candidates: NUM_SELECTED_CANDIDATES, + parachain_bond_reserve_percent: PARACHAIN_BOND_RESERVE_PERCENT, + collator_commission: COLLATOR_COMMISSION, + inflation_config: neuroweb_inflation_config() + } }; serde_json::to_value(&config).expect("Could not build genesis config.")