diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index eccaf24e8..049f03030 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -57,7 +57,7 @@ jobs: run: sudo apt-get install protobuf-compiler - name: Run test suite - run: cargo test --release --workspace + run: cargo test clippy: concurrency: diff --git a/Cargo.lock b/Cargo.lock index 32a65e3f0..dee9d91d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8553,6 +8553,28 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)", ] +[[package]] +name = "pallet-tangle-lst" +version = "25.0.0" +dependencies = [ + "cfg-if", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "pallet-staking 28.0.0", + "parity-scale-codec 3.6.12", + "scale-info", + "smart-default", + "sp-core", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)", + "sp-tracing 16.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)", +] + [[package]] name = "pallet-timestamp" version = "27.0.0" @@ -12383,6 +12405,17 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "smol" version = "2.0.0" @@ -12864,7 +12897,7 @@ dependencies = [ [[package]] name = "sp-crypto-ec-utils" version = "0.10.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "ark-bls12-377", "ark-bls12-377-ext", @@ -12940,7 +12973,7 @@ dependencies = [ [[package]] name = "sp-debug-derive" version = "14.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "proc-macro2", "quote", @@ -12961,7 +12994,7 @@ dependencies = [ [[package]] name = "sp-externalities" version = "0.25.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "environmental", "parity-scale-codec 3.6.12", @@ -13161,7 +13194,7 @@ dependencies = [ [[package]] name = "sp-runtime-interface" version = "24.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "bytes", "impl-trait-for-tuples", @@ -13193,7 +13226,7 @@ dependencies = [ [[package]] name = "sp-runtime-interface-proc-macro" version = "17.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "Inflector", "expander", @@ -13286,7 +13319,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot [[package]] name = "sp-std" version = "14.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" [[package]] name = "sp-storage" @@ -13304,7 +13337,7 @@ dependencies = [ [[package]] name = "sp-storage" version = "19.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "impl-serde", "parity-scale-codec 3.6.12", @@ -13341,7 +13374,7 @@ dependencies = [ [[package]] name = "sp-tracing" version = "16.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "parity-scale-codec 3.6.12", "tracing", @@ -13441,7 +13474,7 @@ dependencies = [ [[package]] name = "sp-wasm-interface" version = "20.0.0" -source = "git+https://github.com/paritytech/polkadot-sdk#aca25a009f7492d3c5ef07d62363f6812688355b" +source = "git+https://github.com/paritytech/polkadot-sdk#ebf4f8d2d590f41817d5d38b2d9b5812a46f2342" dependencies = [ "impl-trait-for-tuples", "log", @@ -14599,6 +14632,7 @@ dependencies = [ "pallet-staking 28.0.0", "pallet-staking-reward-curve", "pallet-sudo", + "pallet-tangle-lst", "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", @@ -14724,6 +14758,7 @@ dependencies = [ "pallet-staking 28.0.0", "pallet-staking-reward-curve", "pallet-sudo", + "pallet-tangle-lst", "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", diff --git a/Cargo.toml b/Cargo.toml index 78108da5a..9345fdd32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ tangle-primitives = { path = "primitives", default-features = false } tangle-crypto-primitives = { path = "primitives/crypto", default-features = false } pallet-staking = { path = "pallets/staking", default-features = false } pallet-staking-reward-curve = { path = "pallets/staking/reward-curve", default-features = false } - +pallet-tangle-lst = { path = "pallets/tangle-lst", default-features = false } primitives-ext = { path = "primitives/ext", default-features = false } evm-tracing-events = { path = "primitives/rpc/evm-tracing-events", default-features = false } rpc-primitives-debug = { path = "primitives/rpc/debug", default-features = false } diff --git a/node/src/chainspec/mainnet.rs b/node/src/chainspec/mainnet.rs index b0a89e91f..4ff6d91c5 100644 --- a/node/src/chainspec/mainnet.rs +++ b/node/src/chainspec/mainnet.rs @@ -310,5 +310,6 @@ fn mainnet_genesis( MultiAddress::Native(TreasuryPalletId::get().into_account_truncating()), )), }, + lst: Default::default(), } } diff --git a/node/src/chainspec/testnet.rs b/node/src/chainspec/testnet.rs index a936b394b..82d9cae03 100644 --- a/node/src/chainspec/testnet.rs +++ b/node/src/chainspec/testnet.rs @@ -416,6 +416,7 @@ fn testnet_genesis( MultiAddress::Native(TreasuryPalletId::get().into_account_truncating()), )), }, + lst: Default::default(), } } diff --git a/pallets/tangle-lst/Cargo.toml b/pallets/tangle-lst/Cargo.toml new file mode 100644 index 000000000..4e853ca17 --- /dev/null +++ b/pallets/tangle-lst/Cargo.toml @@ -0,0 +1,77 @@ +[package] +name = "pallet-tangle-lst" +version = "25.0.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage = "https://substrate.io" +repository.workspace = true +description = "FRAME nomination pools pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# parity +scale-info = { version = "2.10.0", default-features = false, features = [ + "derive", +] } +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [ + "derive", +] } +cfg-if = "1.0" +smart-default = "0.6.0" + +# FRAME +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } +sp-std = { workspace = true, default-features = false } +sp-staking = { workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-io = { workspace = true, default-features = false } +log = { version = "0.4.0", default-features = false } +pallet-staking = { workspace = true } + +# Optional: use for testing and/or fuzzing +pallet-balances = { workspace = true, optional = true } +pallet-assets = { workspace = true, optional = true } +sp-tracing = { workspace = true, optional = true } + +[dev-dependencies] +pallet-balances = { workspace = true } +pallet-assets = { workspace = true } +sp-tracing = { workspace = true } + +[features] +default = ["std"] +fuzzing = ["pallet-balances", "sp-tracing"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances?/std", + "pallet-assets?/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", + "sp-tracing?/std", + "pallet-staking/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances?/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-staking/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances?/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/tangle-lst/src/lib.rs b/pallets/tangle-lst/src/lib.rs new file mode 100644 index 000000000..50a3a8f47 --- /dev/null +++ b/pallets/tangle-lst/src/lib.rs @@ -0,0 +1,1751 @@ +// This file is part of Substrate. + +//! # Nomination Pools for Liquid Staking + +//! A pallet that allows members to delegate their stake to nominating pools. A nomination pool acts +//! as a nominator and nominates validators on behalf of its members. + +//! ## Key Terms + +//! * pool id: A unique identifier for each pool. Set to u32. +//! * bonded pool: Tracks the distribution of actively staked funds. See [`BondedPool`] and +//! [`BondedPoolInner`]. +//! * reward pool: Tracks rewards earned by actively staked funds. See [`RewardPool`] and +//! [`RewardPools`]. +//! * unbonding sub pools: Collection of pools at different phases of the unbonding lifecycle. See +//! [`SubPools`] and [`SubPoolsStorage`]. +//! * members: Accounts that are members of pools. See [`PoolMember`] and [`PoolMembers`]. +//! * roles: Administrative roles of each pool, capable of controlling nomination and the state of +//! the pool. +//! * point: A unit of measure for a member's portion of a pool's funds. Points initially have a +//! ratio of 1 (as set by `POINTS_TO_BALANCE_INIT_RATIO`) to balance, but this can change as +//! slashing occurs. +//! * kick: The act of a pool administrator forcibly ejecting a member. +//! * bonded account: A key-less account id derived from the pool id that acts as the bonded +//! account. +//! * reward account: A similar key-less account that is set as the `Payee` account for the bonded +//! account for all staking rewards. +//! * change rate: The rate at which pool commission can be changed. A change rate consists of a +//! `max_increase` and `min_delay`, dictating the maximum percentage increase that can be applied +//! to the commission per number of blocks. +//! * throttle: An attempted commission increase is throttled if the attempted change falls outside +//! the change rate bounds. + +//! ## Usage + +//! ### Join + +//! An account can stake funds with a nomination pool by calling [`Call::join`]. + +//! ### Claim rewards + +//! After joining a pool, a member can claim rewards by calling [`Call::claim_payout`]. + +//! A pool member can also set a `ClaimPermission` with [`Call::set_claim_permission`], to allow +//! other members to permissionlessly bond or withdraw their rewards by calling +//! [`Call::bond_extra_other`] or [`Call::claim_payout_other`] respectively. + +//! ### Leave + +//! To leave, a member must take two steps: + +//! 1. Call [`Call::unbond`] to start the unbonding process for all or a portion of their funds. +//! 2. Once [`sp_staking::StakingInterface::bonding_duration`] eras have passed, call +//! [`Call::withdraw_unbonded`] to withdraw any funds that are free. + +//! ### Slashes + +//! Slashes are distributed evenly across the bonded pool and the unbonding pools from slash era+1 +//! through the slash apply era. Any member who either unbonded or was actively bonded in this range +//! of eras will be affected by the slash. A member is slashed pro-rata based on its stake relative +//! to the total slash amount. + +//! ### Administration + +//! A pool can be created with the [`Call::create`] or [`Call::create_with_pool_id`] calls. Once +//! created, the pool's nominator or root user must call [`Call::nominate`] to start nominating. + +//! Similar to [`Call::nominate`], [`Call::chill`] will chill the pool in the staking system, and +//! [`Call::pool_withdraw_unbonded`] will withdraw any unbonding chunks of the pool bonded account. + +//! To help facilitate pool administration, the pool has one of three states (see [`PoolState`]): +//! Open, Blocked, or Destroying. + +//! A pool has 4 administrative roles (see [`PoolRoles`]): Depositor, Nominator, Bouncer, and Root. + +//! ### Commission + +//! A pool can optionally have a commission configuration, set by the `root` role with +//! [`Call::set_commission`] and claimed with [`Call::claim_commission`]. Commission is subject to +//! a global maximum and a change rate, which can be set with [`Call::set_commission_max`] and +//! [`Call::set_commission_change_rate`] respectively. + +//! ### Dismantling + +//! A pool is destroyed once all members have fully unbonded and withdrawn, and the depositor has +//! fully unbonded and withdrawn. + +//! ## New Features + +//! ### Liquid Staking Tokens + +//! The pallet now supports the creation of liquid staking tokens for each pool. When a member joins +//! a pool, they receive liquid staking tokens representing their share of the pool. These tokens +//! can be transferred or used in other DeFi applications while the underlying stake remains bonded. + +//! ### Pool Creation with Specific ID + +//! Pools can now be created with a specific ID using the [`Call::create_with_pool_id`] function. +//! This allows for more flexible pool management and integration with external systems. + +//! ### Adjustable Pool Deposit + +//! The [`Call::adjust_pool_deposit`] function allows for topping up the deficit or withdrawing the +//! excess Existential Deposit (ED) from the pool's reward account. This ensures that the pool +//! always has the correct ED, even if the ED requirement changes over time. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Codec; +use frame_support::traits::fungibles; +use frame_support::traits::fungibles::Create; +use frame_support::traits::fungibles::Inspect as FungiblesInspect; +use frame_support::traits::fungibles::Mutate as FungiblesMutate; +use frame_support::traits::tokens::Precision; +use frame_support::traits::Currency; +use frame_support::traits::ExistenceRequirement; +use frame_support::traits::LockableCurrency; +use frame_support::traits::ReservableCurrency; +use frame_support::{ + defensive, defensive_assert, ensure, + pallet_prelude::{MaxEncodedLen, *}, + storage::bounded_btree_map::BoundedBTreeMap, + traits::{ + tokens::Fortitude, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get, + }, + DefaultNoBound, PalletError, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use scale_info::TypeInfo; +use sp_core::U256; +use sp_runtime::traits::AccountIdConversion; +use sp_runtime::traits::{ + AtLeast32BitUnsigned, Bounded, CheckedAdd, Convert, Saturating, StaticLookup, Zero, +}; +use sp_runtime::FixedPointNumber; +use sp_runtime::Perbill; +use sp_staking::{EraIndex, StakingInterface}; +use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, ops::Div, vec::Vec}; + +/// The log target of this pallet. +pub const LOG_TARGET: &str = "runtime::nomination-pools"; +// syntactic sugar for logging. +#[macro_export] +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: $crate::LOG_TARGET, + concat!("[{:?}] 🏊‍♂️ ", $patter), >::block_number() $(, $values)* + ) + }; +} + +#[cfg(any(test, feature = "fuzzing"))] +pub mod mock; +#[cfg(test)] +mod tests; + +pub mod types; +pub mod weights; +pub use pallet::*; +pub use types::*; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::traits::StorageVersion; + use frame_system::{ensure_signed, pallet_prelude::*}; + use sp_runtime::Perbill; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(8); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: weights::WeightInfo; + + /// The currency type used for nomination pool. + type Currency: Currency + + ReservableCurrency + + LockableCurrency; + + /// The overarching freeze reason. + type RuntimeFreezeReason: From; + + /// The type that is used for reward counter. + /// + /// The arithmetic of the reward counter might saturate based on the size of the + /// `Currency::Balance`. If this happens, operations fails. Nonetheless, this type should be + /// chosen such that this failure almost never happens, as if it happens, the pool basically + /// needs to be dismantled (or all pools migrated to a larger `RewardCounter` type, which is + /// a PITA to do). + /// + /// See the inline code docs of `Member::pending_rewards` and `RewardPool::update_recorded` + /// for example analysis. A [`sp_runtime::FixedU128`] should be fine for chains with balance + /// types similar to that of Polkadot and Kusama, in the absence of severe slashing (or + /// prevented via a reasonable `MaxPointsToBalance`), for many many years to come. + type RewardCounter: FixedPointNumber + MaxEncodedLen + TypeInfo + Default + codec::FullCodec; + + /// The nomination pool's pallet id. + #[pallet::constant] + type PalletId: Get; + + /// The maximum pool points-to-balance ratio that an `open` pool can have. + /// + /// This is important in the event slashing takes place and the pool's points-to-balance + /// ratio becomes disproportional. + /// + /// Moreover, this relates to the `RewardCounter` type as well, as the arithmetic operations + /// are a function of number of points, and by setting this value to e.g. 10, you ensure + /// that the total number of points in the system are at most 10 times the total_issuance of + /// the chain, in the absolute worse case. + /// + /// For a value of 10, the threshold would be a pool points-to-balance ratio of 10:1. + /// Such a scenario would also be the equivalent of the pool being 90% slashed. + #[pallet::constant] + type MaxPointsToBalance: Get; + + /// The maximum number of simultaneous unbonding chunks that can exist per member. + #[pallet::constant] + type MaxUnbonding: Get; + + /// Infallible method for converting `Currency::Balance` to `U256`. + type BalanceToU256: Convert, U256>; + + /// Infallible method for converting `U256` to `Currency::Balance`. + type U256ToBalance: Convert>; + + /// The interface for nominating. + type Staking: StakingInterface, AccountId = Self::AccountId>; + + /// The amount of eras a `SubPools::with_era` pool can exist before it gets merged into the + /// `SubPools::no_era` pool. In other words, this is the amount of eras a member will be + /// able to withdraw from an unbonding pool which is guaranteed to have the correct ratio of + /// points to balance; once the `with_era` pool is merged into the `no_era` pool, the ratio + /// can become skewed due to some slashed ratio getting merged in at some point. + type PostUnbondingPoolsWindow: Get; + + /// The maximum length, in bytes, that a pools metadata maybe. + type MaxMetadataLen: Get; + + /// The fungibles trait used for managing fungible assets. + type Fungibles: fungibles::Inspect> + + fungibles::Mutate + + fungibles::Create; + + /// The asset ID type. + type AssetId: AtLeast32BitUnsigned + + Parameter + + Member + + MaybeSerializeDeserialize + + Clone + + Copy + + PartialOrd + + MaxEncodedLen; + + /// The pool ID type. + type PoolId: AtLeast32BitUnsigned + + Parameter + + Member + + MaybeSerializeDeserialize + + Clone + + Copy + + PartialOrd + + MaxEncodedLen; + + /// The origin with privileged access + type ForceOrigin: EnsureOrigin; + } + + /// The sum of funds across all pools. + /// + /// This might be lower but never higher than the sum of `total_balance` of all [`PoolMembers`] + /// because calling `pool_withdraw_unbonded` might decrease the total stake of the pool's + /// `bonded_account` without adjusting the pallet-internal `UnbondingPool`'s. + #[pallet::storage] + pub type TotalValueLocked = StorageValue<_, BalanceOf, ValueQuery>; + + /// Minimum amount to bond to join a pool. + #[pallet::storage] + pub type MinJoinBond = StorageValue<_, BalanceOf, ValueQuery>; + + /// Minimum bond required to create a pool. + /// + /// This is the amount that the depositor must put as their initial stake in the pool, as an + /// indication of "skin in the game". + /// + /// This is the value that will always exist in the staking ledger of the pool bonded account + /// while all other accounts leave. + #[pallet::storage] + pub type MinCreateBond = StorageValue<_, BalanceOf, ValueQuery>; + + /// Maximum number of nomination pools that can exist. If `None`, then an unbounded number of + /// pools can exist. + #[pallet::storage] + pub type MaxPools = StorageValue<_, u32, OptionQuery>; + + /// The maximum commission that can be charged by a pool. Used on commission payouts to bound + /// pool commissions that are > `GlobalMaxCommission`, necessary if a future + /// `GlobalMaxCommission` is lower than some current pool commissions. + #[pallet::storage] + pub type GlobalMaxCommission = StorageValue<_, Perbill, OptionQuery>; + + /// Storage for bonded pools. + // To get or insert a pool see [`BondedPool::get`] and [`BondedPool::put`] + #[pallet::storage] + pub type BondedPools = + CountedStorageMap<_, Twox64Concat, PoolId, BondedPoolInner>; + + /// Reward pools. This is where there rewards for each pool accumulate. When a members payout is + /// claimed, the balance comes out fo the reward pool. Keyed by the bonded pools account. + #[pallet::storage] + pub type RewardPools = CountedStorageMap<_, Twox64Concat, PoolId, RewardPool>; + + /// Groups of unbonding pools. Each group of unbonding pools belongs to a + /// bonded pool, hence the name sub-pools. Keyed by the bonded pools account. + #[pallet::storage] + pub type SubPoolsStorage = CountedStorageMap<_, Twox64Concat, PoolId, SubPools>; + + /// Metadata for the pool. + #[pallet::storage] + pub type Metadata = + CountedStorageMap<_, Twox64Concat, PoolId, BoundedVec, ValueQuery>; + + /// Ever increasing number of all pools created so far. + #[pallet::storage] + pub type LastPoolId = StorageValue<_, u32, ValueQuery>; + + /// Unbonding members. + /// + /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. + #[pallet::storage] + pub type UnbondingMembers = + CountedStorageMap<_, Twox64Concat, T::AccountId, PoolMember>; + + /// A reverse lookup from the pool's account id to its id. + /// + /// This is only used for slashing. In all other instances, the pool id is used, and the + /// accounts are deterministically derived from it. + #[pallet::storage] + pub type ReversePoolIdLookup = + CountedStorageMap<_, Twox64Concat, T::AccountId, PoolId, OptionQuery>; + + /// Map from a pool member account to their opted claim permission. + #[pallet::storage] + pub type ClaimPermissions = + StorageMap<_, Twox64Concat, T::AccountId, ClaimPermission, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub min_join_bond: BalanceOf, + pub min_create_bond: BalanceOf, + pub max_pools: Option, + pub max_members_per_pool: Option, + pub max_members: Option, + pub global_max_commission: Option, + } + + impl Default for GenesisConfig { + fn default() -> Self { + Self { + min_join_bond: Zero::zero(), + min_create_bond: Zero::zero(), + max_pools: Some(16), + max_members_per_pool: Some(32), + max_members: Some(16 * 32), + global_max_commission: None, + } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + MinJoinBond::::put(self.min_join_bond); + MinCreateBond::::put(self.min_create_bond); + + if let Some(max_pools) = self.max_pools { + MaxPools::::put(max_pools); + } + if let Some(global_max_commission) = self.global_max_commission { + GlobalMaxCommission::::put(global_max_commission); + } + } + } + + /// Events of this pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// A pool has been created. + Created { depositor: T::AccountId, pool_id: PoolId }, + /// A member has became bonded in a pool. + Bonded { member: T::AccountId, pool_id: PoolId, bonded: BalanceOf, joined: bool }, + /// A payout has been made to a member. + PaidOut { member: T::AccountId, pool_id: PoolId, payout: BalanceOf }, + /// A member has unbonded from their pool. + /// + /// - `balance` is the corresponding balance of the number of points that has been + /// requested to be unbonded (the argument of the `unbond` transaction) from the bonded + /// pool. + /// - `points` is the number of points that are issued as a result of `balance` being + /// dissolved into the corresponding unbonding pool. + /// - `era` is the era in which the balance will be unbonded. + /// In the absence of slashing, these values will match. In the presence of slashing, the + /// number of points that are issued in the unbonding pool will be less than the amount + /// requested to be unbonded. + Unbonded { + member: T::AccountId, + pool_id: PoolId, + balance: BalanceOf, + points: BalanceOf, + era: EraIndex, + }, + /// A member has withdrawn from their pool. + /// + /// The given number of `points` have been dissolved in return of `balance`. + /// + /// Similar to `Unbonded` event, in the absence of slashing, the ratio of point to balance + /// will be 1. + Withdrawn { + member: T::AccountId, + pool_id: PoolId, + balance: BalanceOf, + points: BalanceOf, + }, + /// A pool has been destroyed. + Destroyed { pool_id: PoolId }, + /// The state of a pool has changed + StateChanged { pool_id: PoolId, new_state: PoolState }, + /// A member has been removed from a pool. + /// + /// The removal can be voluntary (withdrawn all unbonded funds) or involuntary (kicked). + MemberRemoved { pool_id: PoolId, member: T::AccountId }, + /// The roles of a pool have been updated to the given new roles. Note that the depositor + /// can never change. + RolesUpdated { + root: Option, + bouncer: Option, + nominator: Option, + }, + /// The active balance of pool `pool_id` has been slashed to `balance`. + PoolSlashed { pool_id: PoolId, balance: BalanceOf }, + /// The unbond pool at `era` of pool `pool_id` has been slashed to `balance`. + UnbondingPoolSlashed { pool_id: PoolId, era: EraIndex, balance: BalanceOf }, + /// A pool's commission setting has been changed. + PoolCommissionUpdated { pool_id: PoolId, current: Option<(Perbill, T::AccountId)> }, + /// A pool's maximum commission setting has been changed. + PoolMaxCommissionUpdated { pool_id: PoolId, max_commission: Perbill }, + /// A pool's commission `change_rate` has been changed. + PoolCommissionChangeRateUpdated { + pool_id: PoolId, + change_rate: CommissionChangeRate>, + }, + /// Pool commission claim permission has been updated. + PoolCommissionClaimPermissionUpdated { + pool_id: PoolId, + permission: Option>, + }, + /// Pool commission has been claimed. + PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf }, + /// Topped up deficit in frozen ED of the reward pool. + MinBalanceDeficitAdjusted { pool_id: PoolId, amount: BalanceOf }, + /// Claimed excess frozen ED of af the reward pool. + MinBalanceExcessAdjusted { pool_id: PoolId, amount: BalanceOf }, + } + + #[pallet::error] + #[cfg_attr(test, derive(PartialEq))] + pub enum Error { + /// A (bonded) pool id does not exist. + PoolNotFound, + /// An account is not a member. + PoolMemberNotFound, + /// A reward pool does not exist. In all cases this is a system logic error. + RewardPoolNotFound, + /// A sub pool does not exist. + SubPoolsNotFound, + /// The member is fully unbonded (and thus cannot access the bonded and reward pool + /// anymore to, for example, collect rewards). + FullyUnbonding, + /// The member cannot unbond further chunks due to reaching the limit. + MaxUnbondingLimit, + /// None of the funds can be withdrawn yet because the bonding duration has not passed. + CannotWithdrawAny, + /// The amount does not meet the minimum bond to either join or create a pool. + /// + /// The depositor can never unbond to a value less than `Pallet::depositor_min_bond`. The + /// caller does not have nominating permissions for the pool. Members can never unbond to a + /// value below `MinJoinBond`. + MinimumBondNotMet, + /// The transaction could not be executed due to overflow risk for the pool. + OverflowRisk, + /// A pool must be in [`PoolState::Destroying`] in order for the depositor to unbond or for + /// other members to be permissionlessly unbonded. + NotDestroying, + /// The caller does not have nominating permissions for the pool. + NotNominator, + /// Either a) the caller cannot make a valid kick or b) the pool is not destroying. + NotKickerOrDestroying, + /// The pool is not open to join + NotOpen, + /// The system is maxed out on pools. + MaxPools, + /// Too many members in the pool or system. + MaxPoolMembers, + /// The pools state cannot be changed. + CanNotChangeState, + /// The caller does not have adequate permissions. + DoesNotHavePermission, + /// Metadata exceeds [`Config::MaxMetadataLen`] + MetadataExceedsMaxLen, + /// Some error occurred that should never happen. This should be reported to the + /// maintainers. + Defensive(DefensiveError), + /// Partial unbonding now allowed permissionlessly. + PartialUnbondNotAllowedPermissionlessly, + /// The pool's max commission cannot be set higher than the existing value. + MaxCommissionRestricted, + /// The supplied commission exceeds the max allowed commission. + CommissionExceedsMaximum, + /// The supplied commission exceeds global maximum commission. + CommissionExceedsGlobalMaximum, + /// Not enough blocks have surpassed since the last commission update. + CommissionChangeThrottled, + /// The submitted changes to commission change rate are not allowed. + CommissionChangeRateNotAllowed, + /// There is no pending commission to claim. + NoPendingCommission, + /// No commission current has been set. + NoCommissionCurrentSet, + /// Pool id currently in use. + PoolIdInUse, + /// Pool id provided is not correct/usable. + InvalidPoolId, + /// Bonding extra is restricted to the exact pending reward amount. + BondExtraRestricted, + /// No imbalance in the ED deposit for the pool. + NothingToAdjust, + /// Pool token creation failed. + PoolTokenCreationFailed, + /// No balance to unbond. + NoBalanceToUnbond, + } + + #[derive(Encode, Decode, PartialEq, TypeInfo, PalletError, RuntimeDebug)] + pub enum DefensiveError { + /// There isn't enough space in the unbond pool. + NotEnoughSpaceInUnbondPool, + /// A (bonded) pool id does not exist. + PoolNotFound, + /// A reward pool does not exist. In all cases this is a system logic error. + RewardPoolNotFound, + /// A sub pool does not exist. + SubPoolsNotFound, + /// The bonded account should only be killed by the staking system when the depositor is + /// withdrawing + BondedStashKilledPrematurely, + } + + impl From for Error { + fn from(e: DefensiveError) -> Error { + Error::::Defensive(e) + } + } + + /// A reason for freezing funds. + #[pallet::composite_enum] + pub enum FreezeReason { + /// Pool reward account is restricted from going below Existential Deposit. + #[codec(index = 0)] + PoolMinBalance, + } + + #[pallet::call] + impl Pallet { + /// Stake funds with a pool. The amount to bond is transferred from the member to the + /// pools account and immediately increases the pools bond. + /// + /// # Note + /// + /// * This call will *not* dust the member account, so the member must have at least + /// `existential deposit + amount` in their account. + /// * Only a pool with [`PoolState::Open`] can be joined + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::join())] + pub fn join( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + pool_id: PoolId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(amount >= MinJoinBond::::get(), Error::::MinimumBondNotMet); + + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + bonded_pool.ok_to_join()?; + + let mut reward_pool = RewardPools::::get(pool_id) + .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; + + // IMPORTANT: reward pool records must be updated with the old points. + reward_pool.update_records( + pool_id, + bonded_pool.points(), + bonded_pool.commission.current(), + )?; + + bonded_pool.try_bond_funds(&who, amount, BondType::Later)?; + + Self::deposit_event(Event::::Bonded { + member: who, + pool_id, + bonded: amount, + joined: true, + }); + + bonded_pool.put(); + RewardPools::::insert(pool_id, reward_pool); + + Ok(()) + } + + /// Bond `extra` more funds from `origin` into the pool to which they already belong. + /// + /// Additional funds can come from either the free balance of the account, of from the + /// accumulated rewards, see [`BondExtra`]. + /// + /// Bonding extra funds implies an automatic payout of all pending rewards as well. + /// See `bond_extra_other` to bond pending rewards of `other` members. + // NOTE: this transaction is implemented with the sole purpose of readability and + // correctness, not optimization. We read/write several storage items multiple times instead + // of just once, in the spirit reusing code. + #[pallet::call_index(1)] + #[pallet::weight( + T::WeightInfo::bond_extra_transfer() + .max(T::WeightInfo::bond_extra_other()) + )] + pub fn bond_extra( + origin: OriginFor, + pool_id: PoolId, + extra: BondExtra>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_bond_extra(who.clone(), who, pool_id, extra) + } + + /// Unbond up to `unbonding_points` of the `member_account`'s funds from the pool. It + /// implicitly collects the rewards one last time, since not doing so would mean some + /// rewards would be forfeited. + /// + /// Under certain conditions, this call can be dispatched permissionlessly (i.e. by any + /// account). + /// + /// # Conditions for a permissionless dispatch. + /// + /// * The pool is blocked and the caller is either the root or bouncer. This is refereed to + /// as a kick. + /// * The pool is destroying and the member is not the depositor. + /// * The pool is destroying, the member is the depositor and no other members are in the + /// pool. + /// + /// ## Conditions for permissioned dispatch (i.e. the caller is also the + /// `member_account`): + /// + /// * The caller is not the depositor. + /// * The caller is the depositor, the pool is destroying and no other members are in the + /// pool. + /// + /// # Note + /// + /// If there are too many unlocking chunks to unbond with the pool account, + /// [`Call::pool_withdraw_unbonded`] can be called to try and minimize unlocking chunks. + /// The [`StakingInterface::unbond`] will implicitly call [`Call::pool_withdraw_unbonded`] + /// to try to free chunks if necessary (ie. if unbound was called and no unlocking chunks + /// are available). However, it may not be possible to release the current unlocking chunks, + /// in which case, the result of this call will likely be the `NoMoreChunks` error from the + /// staking system. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::unbond())] + pub fn unbond( + origin: OriginFor, + member_account: AccountIdLookupOf, + pool_id: PoolId, + #[pallet::compact] unbonding_points: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let member_account = T::Lookup::lookup(member_account)?; + + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + + let total_points = T::Fungibles::balance(pool_id.into(), &member_account); + + ensure!(total_points >= unbonding_points, Error::::NoBalanceToUnbond); + + bonded_pool.ok_to_unbond_with(&who, &member_account, total_points, unbonding_points)?; + + // let burn the pool tokens + T::Fungibles::burn_from( + pool_id.into(), + &member_account, + unbonding_points, + Precision::Exact, + Fortitude::Force, + )?; + + let current_era = T::Staking::current_era(); + let unbond_era = T::Staking::bonding_duration().saturating_add(current_era); + + // Unbond in the actual underlying nominator. + let unbonding_balance = bonded_pool.dissolve(unbonding_points); + T::Staking::unbond(&bonded_pool.bonded_account(), unbonding_balance)?; + + // Note that we lazily create the unbonding pools here if they don't already exist + let mut sub_pools = SubPoolsStorage::::get(pool_id) + .unwrap_or_default() + .maybe_merge_pools(current_era); + + // Update the unbond pool associated with the current era with the unbonded funds. Note + // that we lazily create the unbond pool if it does not yet exist. + if !sub_pools.with_era.contains_key(&unbond_era) { + sub_pools + .with_era + .try_insert(unbond_era, UnbondPool::default()) + // The above call to `maybe_merge_pools` should ensure there is + // always enough space to insert. + .defensive_map_err::, _>(|_| { + DefensiveError::NotEnoughSpaceInUnbondPool.into() + })?; + } + + let points_unbonded = sub_pools + .with_era + .get_mut(&unbond_era) + // The above check ensures the pool exists. + .defensive_ok_or::>(DefensiveError::PoolNotFound.into())? + .issue(unbonding_balance); + + // Try and unbond in the member map. + UnbondingMembers::::try_mutate( + member_account.clone(), + |member| -> DispatchResult { + let member = member.get_or_insert_with(Default::default); + member + .unbonding_eras + .try_insert(unbond_era, points_unbonded) + .map(|old| { + if old.is_some() { + defensive!("value checked to not exist in the map; qed"); + } + }) + .map_err(|_| Error::::MaxUnbondingLimit)?; + Ok(()) + }, + )?; + + Self::deposit_event(Event::::Unbonded { + member: member_account.clone(), + pool_id, + points: points_unbonded, + balance: unbonding_balance, + era: unbond_era, + }); + + // Now that we know everything has worked write the items to storage. + SubPoolsStorage::insert(pool_id, sub_pools); + Ok(()) + } + + /// Call `withdraw_unbonded` for the pools account. This call can be made by any account. + /// + /// This is useful if there are too many unlocking chunks to call `unbond`, and some + /// can be cleared by withdrawing. In the case there are too many unlocking chunks, the user + /// would probably see an error like `NoMoreChunks` emitted from the staking system when + /// they attempt to unbond. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::pool_withdraw_unbonded(*num_slashing_spans))] + pub fn pool_withdraw_unbonded( + origin: OriginFor, + pool_id: PoolId, + num_slashing_spans: u32, + ) -> DispatchResult { + let _ = ensure_signed(origin)?; + let pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + + // For now we only allow a pool to withdraw unbonded if its not destroying. If the pool + // is destroying then `withdraw_unbonded` can be used. + ensure!(pool.state != PoolState::Destroying, Error::::NotDestroying); + pool.withdraw_from_staking(num_slashing_spans)?; + + Ok(()) + } + + /// Withdraw unbonded funds from `member_account`. If no bonded funds can be unbonded, an + /// error is returned. + /// + /// Under certain conditions, this call can be dispatched permissionlessly (i.e. by any + /// account). + /// + /// # Conditions for a permissionless dispatch + /// + /// * The pool is in destroy mode and the target is not the depositor. + /// * The target is the depositor and they are the only member in the sub pools. + /// * The pool is blocked and the caller is either the root or bouncer. + /// + /// # Conditions for permissioned dispatch + /// + /// * The caller is the target and they are not the depositor. + /// + /// # Note + /// + /// If the target is the depositor, the pool will be destroyed. + #[pallet::call_index(5)] + #[pallet::weight( + T::WeightInfo::withdraw_unbonded_kill(*num_slashing_spans) + )] + pub fn withdraw_unbonded( + origin: OriginFor, + member_account: AccountIdLookupOf, + pool_id: PoolId, + num_slashing_spans: u32, + ) -> DispatchResultWithPostInfo { + let caller = ensure_signed(origin)?; + let member_account = T::Lookup::lookup(member_account)?; + let mut member = UnbondingMembers::::get(&member_account) + .ok_or(Error::::PoolMemberNotFound)?; + let current_era = T::Staking::current_era(); + + let bonded_pool = BondedPool::::get(member.pool_id) + .defensive_ok_or::>(DefensiveError::PoolNotFound.into())?; + let mut sub_pools = + SubPoolsStorage::::get(member.pool_id).ok_or(Error::::SubPoolsNotFound)?; + + bonded_pool.ok_to_withdraw_unbonded_with(&caller, &member_account)?; + + // NOTE: must do this after we have done the `ok_to_withdraw_unbonded_other_with` check. + let withdrawn_points = member.withdraw_unlocked(current_era); + ensure!(!withdrawn_points.is_empty(), Error::::CannotWithdrawAny); + + // Before calculating the `balance_to_unbond`, we call withdraw unbonded to ensure the + // `transferrable_balance` is correct. + let stash_killed = bonded_pool.withdraw_from_staking(num_slashing_spans)?; + + // defensive-only: the depositor puts enough funds into the stash so that it will only + // be destroyed when they are leaving. + ensure!( + !stash_killed || caller == bonded_pool.roles.depositor, + Error::::Defensive(DefensiveError::BondedStashKilledPrematurely) + ); + + let mut sum_unlocked_points: BalanceOf = Zero::zero(); + let balance_to_unbond = withdrawn_points + .iter() + .fold(BalanceOf::::zero(), |accumulator, (era, unlocked_points)| { + sum_unlocked_points = sum_unlocked_points.saturating_add(*unlocked_points); + if let Some(era_pool) = sub_pools.with_era.get_mut(era) { + let balance_to_unbond = era_pool.dissolve(*unlocked_points); + if era_pool.points.is_zero() { + sub_pools.with_era.remove(era); + } + accumulator.saturating_add(balance_to_unbond) + } else { + // A pool does not belong to this era, so it must have been merged to the + // era-less pool. + accumulator.saturating_add(sub_pools.no_era.dissolve(*unlocked_points)) + } + }) + // A call to this transaction may cause the pool's stash to get dusted. If this + // happens before the last member has withdrawn, then all subsequent withdraws will + // be 0. However the unbond pools do no get updated to reflect this. In the + // aforementioned scenario, this check ensures we don't try to withdraw funds that + // don't exist. This check is also defensive in cases where the unbond pool does not + // update its balance (e.g. a bug in the slashing hook.) We gracefully proceed in + // order to ensure members can leave the pool and it can be destroyed. + .min(bonded_pool.transferable_balance()); + + T::Currency::transfer( + &bonded_pool.bonded_account(), + &member_account, + balance_to_unbond, + ExistenceRequirement::AllowDeath, + ) + .defensive()?; + + Self::deposit_event(Event::::Withdrawn { + member: member_account.clone(), + pool_id: member.pool_id, + points: sum_unlocked_points, + balance: balance_to_unbond, + }); + + let post_info_weight = if member.unbonding_points().is_zero() { + // member being reaped. + UnbondingMembers::::remove(&member_account); + Self::deposit_event(Event::::MemberRemoved { + pool_id, + member: member_account.clone(), + }); + + if member_account == bonded_pool.roles.depositor { + Pallet::::dissolve_pool(bonded_pool); + None + } else { + SubPoolsStorage::::insert(member.pool_id, sub_pools); + Some(T::WeightInfo::withdraw_unbonded_update(num_slashing_spans)) + } + } else { + // we certainly don't need to delete any pools, because no one is being removed. + SubPoolsStorage::::insert(pool_id, sub_pools); + UnbondingMembers::::insert(&member_account, member); + Some(T::WeightInfo::withdraw_unbonded_update(num_slashing_spans)) + }; + + Ok(post_info_weight.into()) + } + + /// Create a new delegation pool. + /// + /// # Arguments + /// + /// * `amount` - The amount of funds to delegate to the pool. This also acts of a sort of + /// deposit since the pools creator cannot fully unbond funds until the pool is being + /// destroyed. + /// * `index` - A disambiguation index for creating the account. Likely only useful when + /// creating multiple pools in the same extrinsic. + /// * `root` - The account to set as [`PoolRoles::root`]. + /// * `nominator` - The account to set as the [`PoolRoles::nominator`]. + /// * `bouncer` - The account to set as the [`PoolRoles::bouncer`]. + /// + /// # Note + /// + /// In addition to `amount`, the caller will transfer the existential deposit; so the caller + /// needs at have at least `amount + existential_deposit` transferable. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::create())] + pub fn create( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + root: AccountIdLookupOf, + nominator: AccountIdLookupOf, + bouncer: AccountIdLookupOf, + ) -> DispatchResult { + let depositor = ensure_signed(origin)?; + + let pool_id = LastPoolId::::try_mutate::<_, Error, _>(|id| { + *id = id.checked_add(1).ok_or(Error::::OverflowRisk)?; + Ok(*id) + })?; + + Self::do_create(depositor, amount, root, nominator, bouncer, pool_id) + } + + /// Create a new delegation pool with a previously used pool id + /// + /// # Arguments + /// + /// same as `create` with the inclusion of + /// * `pool_id` - `A valid PoolId. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::create())] + pub fn create_with_pool_id( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + root: AccountIdLookupOf, + nominator: AccountIdLookupOf, + bouncer: AccountIdLookupOf, + pool_id: PoolId, + ) -> DispatchResult { + let depositor = ensure_signed(origin)?; + + ensure!(!BondedPools::::contains_key(pool_id), Error::::PoolIdInUse); + ensure!(pool_id < LastPoolId::::get(), Error::::InvalidPoolId); + + Self::do_create(depositor, amount, root, nominator, bouncer, pool_id) + } + + /// Nominate on behalf of the pool. + /// + /// The dispatch origin of this call must be signed by the pool nominator or the pool + /// root role. + /// + /// This directly forward the call to the staking pallet, on behalf of the pool bonded + /// account. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::nominate(validators.len() as u32))] + pub fn nominate( + origin: OriginFor, + pool_id: PoolId, + validators: Vec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_nominate(&who), Error::::NotNominator); + T::Staking::nominate(&bonded_pool.bonded_account(), validators) + } + + /// Set a new state for the pool. + /// + /// If a pool is already in the `Destroying` state, then under no condition can its state + /// change again. + /// + /// The dispatch origin of this call must be either: + /// + /// 1. signed by the bouncer, or the root role of the pool, + /// 2. if the pool conditions to be open are NOT met (as described by `ok_to_be_open`), and + /// then the state of the pool can be permissionlessly changed to `Destroying`. + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::set_state())] + pub fn set_state( + origin: OriginFor, + pool_id: PoolId, + state: PoolState, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.state != PoolState::Destroying, Error::::CanNotChangeState); + + if bonded_pool.can_toggle_state(&who) { + bonded_pool.set_state(state); + } else if bonded_pool.ok_to_be_open().is_err() && state == PoolState::Destroying { + // If the pool has bad properties, then anyone can set it as destroying + bonded_pool.set_state(PoolState::Destroying); + } else { + Err(Error::::CanNotChangeState)?; + } + + bonded_pool.put(); + + Ok(()) + } + + /// Set a new metadata for the pool. + /// + /// The dispatch origin of this call must be signed by the bouncer, or the root role of the + /// pool. + #[pallet::call_index(10)] + #[pallet::weight(T::WeightInfo::set_metadata(metadata.len() as u32))] + pub fn set_metadata( + origin: OriginFor, + pool_id: PoolId, + metadata: Vec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let metadata: BoundedVec<_, _> = + metadata.try_into().map_err(|_| Error::::MetadataExceedsMaxLen)?; + ensure!( + BondedPool::::get(pool_id) + .ok_or(Error::::PoolNotFound)? + .can_set_metadata(&who), + Error::::DoesNotHavePermission + ); + + Metadata::::mutate(pool_id, |pool_meta| *pool_meta = metadata); + + Ok(()) + } + + /// Update configurations for the nomination pools. The origin for this call must be + /// Root. + /// + /// # Arguments + /// + /// * `min_join_bond` - Set [`MinJoinBond`]. + /// * `min_create_bond` - Set [`MinCreateBond`]. + /// * `max_pools` - Set [`MaxPools`]. + /// * `max_members` - Set [`MaxPoolMembers`]. + /// * `max_members_per_pool` - Set [`MaxPoolMembersPerPool`]. + /// * `global_max_commission` - Set [`GlobalMaxCommission`]. + #[pallet::call_index(11)] + #[pallet::weight(T::WeightInfo::set_configs())] + pub fn set_configs( + origin: OriginFor, + min_join_bond: ConfigOp>, + min_create_bond: ConfigOp>, + max_pools: ConfigOp, + global_max_commission: ConfigOp, + ) -> DispatchResult { + ensure_root(origin)?; + + macro_rules! config_op_exp { + ($storage:ty, $op:ident) => { + match $op { + ConfigOp::Noop => (), + ConfigOp::Set(v) => <$storage>::put(v), + ConfigOp::Remove => <$storage>::kill(), + } + }; + } + + config_op_exp!(MinJoinBond::, min_join_bond); + config_op_exp!(MinCreateBond::, min_create_bond); + config_op_exp!(MaxPools::, max_pools); + config_op_exp!(GlobalMaxCommission::, global_max_commission); + Ok(()) + } + + /// Update the roles of the pool. + /// + /// The root is the only entity that can change any of the roles, including itself, + /// excluding the depositor, who can never change. + /// + /// It emits an event, notifying UIs of the role change. This event is quite relevant to + /// most pool members and they should be informed of changes to pool roles. + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::update_roles())] + pub fn update_roles( + origin: OriginFor, + pool_id: PoolId, + new_root: ConfigOp, + new_nominator: ConfigOp, + new_bouncer: ConfigOp, + ) -> DispatchResult { + let mut bonded_pool = match ensure_root(origin.clone()) { + Ok(()) => BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?, + Err(frame_support::error::BadOrigin) => { + let who = ensure_signed(origin)?; + let bonded_pool = + BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_update_roles(&who), Error::::DoesNotHavePermission); + bonded_pool + }, + }; + + match new_root { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.root = None, + ConfigOp::Set(v) => bonded_pool.roles.root = Some(v), + }; + match new_nominator { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.nominator = None, + ConfigOp::Set(v) => bonded_pool.roles.nominator = Some(v), + }; + match new_bouncer { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.bouncer = None, + ConfigOp::Set(v) => bonded_pool.roles.bouncer = Some(v), + }; + + Self::deposit_event(Event::::RolesUpdated { + root: bonded_pool.roles.root.clone(), + nominator: bonded_pool.roles.nominator.clone(), + bouncer: bonded_pool.roles.bouncer.clone(), + }); + + bonded_pool.put(); + Ok(()) + } + + /// Chill on behalf of the pool. + /// + /// The dispatch origin of this call must be signed by the pool nominator or the pool + /// root role, same as [`Pallet::nominate`]. + /// + /// This directly forward the call to the staking pallet, on behalf of the pool bonded + /// account. + #[pallet::call_index(13)] + #[pallet::weight(T::WeightInfo::chill())] + pub fn chill(origin: OriginFor, pool_id: PoolId) -> DispatchResult { + let who = ensure_signed(origin)?; + let bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_nominate(&who), Error::::NotNominator); + T::Staking::chill(&bonded_pool.bonded_account()) + } + + /// `origin` bonds funds from `extra` for some pool member `member` into their respective + /// pools. + /// + /// `origin` can bond extra funds from free balance or pending rewards when `origin == + /// other`. + /// + /// In the case of `origin != other`, `origin` can only bond extra pending rewards of + /// `other` members assuming set_claim_permission for the given member is + /// `PermissionlessAll` or `PermissionlessCompound`. + #[pallet::call_index(14)] + #[pallet::weight( + T::WeightInfo::bond_extra_transfer() + .max(T::WeightInfo::bond_extra_other()) + )] + pub fn bond_extra_other( + origin: OriginFor, + member: AccountIdLookupOf, + pool_id: PoolId, + extra: BondExtra>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_bond_extra(who, T::Lookup::lookup(member)?, pool_id, extra) + } + + /// Set the commission of a pool. + // + /// Both a commission percentage and a commission payee must be provided in the `current` + /// tuple. Where a `current` of `None` is provided, any current commission will be removed. + /// + /// - If a `None` is supplied to `new_commission`, existing commission will be removed. + #[pallet::call_index(17)] + #[pallet::weight(T::WeightInfo::set_commission())] + pub fn set_commission( + origin: OriginFor, + pool_id: PoolId, + new_commission: Option<(Perbill, T::AccountId)>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + let mut reward_pool = RewardPools::::get(pool_id) + .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; + // IMPORTANT: make sure that everything up to this point is using the current commission + // before it updates. Note that `try_update_current` could still fail at this point. + reward_pool.update_records( + pool_id, + bonded_pool.points(), + bonded_pool.commission.current(), + )?; + RewardPools::insert(pool_id, reward_pool); + + bonded_pool.commission.try_update_current(&new_commission)?; + bonded_pool.put(); + Self::deposit_event(Event::::PoolCommissionUpdated { + pool_id, + current: new_commission, + }); + Ok(()) + } + + /// Set the maximum commission of a pool. + /// + /// - Initial max can be set to any `Perbill`, and only smaller values thereafter. + /// - Current commission will be lowered in the event it is higher than a new max + /// commission. + #[pallet::call_index(18)] + #[pallet::weight(T::WeightInfo::set_commission_max())] + pub fn set_commission_max( + origin: OriginFor, + pool_id: PoolId, + max_commission: Perbill, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + bonded_pool.commission.try_update_max(pool_id, max_commission)?; + bonded_pool.put(); + + Self::deposit_event(Event::::PoolMaxCommissionUpdated { pool_id, max_commission }); + Ok(()) + } + + /// Set the commission change rate for a pool. + /// + /// Initial change rate is not bounded, whereas subsequent updates can only be more + /// restrictive than the current. + #[pallet::call_index(19)] + #[pallet::weight(T::WeightInfo::set_commission_change_rate())] + pub fn set_commission_change_rate( + origin: OriginFor, + pool_id: PoolId, + change_rate: CommissionChangeRate>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + bonded_pool.commission.try_update_change_rate(change_rate)?; + bonded_pool.put(); + + Self::deposit_event(Event::::PoolCommissionChangeRateUpdated { + pool_id, + change_rate, + }); + Ok(()) + } + + /// Claim pending commission. + /// + /// The dispatch origin of this call must be signed by the `root` role of the pool. Pending + /// commission is paid out and added to total claimed commission`. Total pending commission + /// is reset to zero. the current. + #[pallet::call_index(20)] + #[pallet::weight(T::WeightInfo::claim_commission())] + pub fn claim_commission(origin: OriginFor, pool_id: PoolId) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_claim_commission(who, pool_id) + } + + /// Top up the deficit or withdraw the excess ED from the pool. + /// + /// When a pool is created, the pool depositor transfers ED to the reward account of the + /// pool. ED is subject to change and over time, the deposit in the reward account may be + /// insufficient to cover the ED deficit of the pool or vice-versa where there is excess + /// deposit to the pool. This call allows anyone to adjust the ED deposit of the + /// pool by either topping up the deficit or claiming the excess. + #[pallet::call_index(21)] + #[pallet::weight(T::WeightInfo::adjust_pool_deposit())] + pub fn adjust_pool_deposit(origin: OriginFor, pool_id: PoolId) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_adjust_pool_deposit(who, pool_id) + } + + /// Set or remove a pool's commission claim permission. + /// + /// Determines who can claim the pool's pending commission. Only the `Root` role of the pool + /// is able to conifigure commission claim permissions. + #[pallet::call_index(22)] + #[pallet::weight(T::WeightInfo::set_commission_claim_permission())] + pub fn set_commission_claim_permission( + origin: OriginFor, + pool_id: PoolId, + permission: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_manage_commission(&who), Error::::DoesNotHavePermission); + + bonded_pool.commission.claim_permission.clone_from(&permission); + bonded_pool.put(); + + Self::deposit_event(Event::::PoolCommissionClaimPermissionUpdated { + pool_id, + permission, + }); + + Ok(()) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> { + Self::do_try_state(u8::MAX) + } + + fn integrity_test() { + assert!( + T::MaxPointsToBalance::get() > 0, + "Minimum points to balance ratio must be greater than 0" + ); + assert!( + T::Staking::bonding_duration() < TotalUnbondingPools::::get(), + "There must be more unbonding pools then the bonding duration / + so a slash can be applied to relevant unboding pools. (We assume / + the bonding duration > slash deffer duration.", + ); + } + } +} + +impl Pallet { + /// The amount of bond that MUST REMAIN IN BONDED in ALL POOLS. + /// + /// It is the responsibility of the depositor to put these funds into the pool initially. Upon + /// unbond, they can never unbond to a value below this amount. + /// + /// It is essentially `max { MinNominatorBond, MinCreateBond, MinJoinBond }`, where the former + /// is coming from the staking pallet and the latter two are configured in this pallet. + pub fn depositor_min_bond() -> BalanceOf { + T::Staking::minimum_nominator_bond() + .max(MinCreateBond::::get()) + .max(MinJoinBond::::get()) + .max(T::Currency::minimum_balance()) + } + /// Remove everything related to the given bonded pool. + /// + /// Metadata and all of the sub-pools are also deleted. All accounts are dusted and the leftover + /// of the reward account is returned to the depositor. + pub fn dissolve_pool(bonded_pool: BondedPool) { + let reward_account = bonded_pool.reward_account(); + let bonded_account = bonded_pool.bonded_account(); + + ReversePoolIdLookup::::remove(&bonded_account); + RewardPools::::remove(bonded_pool.id); + SubPoolsStorage::::remove(bonded_pool.id); + + // remove the ED restriction from the pool reward account. + let _ = Self::unfreeze_pool_deposit(&bonded_pool.reward_account()).defensive(); + + // Kill accounts from storage by making their balance go below ED. We assume that the + // accounts have no references that would prevent destruction once we get to this point. We + // don't work with the system pallet directly, but + // 1. we drain the reward account and kill it. This account should never have any extra + // consumers anyway. + // 2. the bonded account should become a 'killed stash' in the staking system, and all of + // its consumers removed. + defensive_assert!( + frame_system::Pallet::::consumers(&reward_account) == 0, + "reward account of dissolving pool should have no consumers" + ); + defensive_assert!( + frame_system::Pallet::::consumers(&bonded_account) == 0, + "bonded account of dissolving pool should have no consumers" + ); + defensive_assert!( + T::Staking::total_stake(&bonded_account).unwrap_or_default() == Zero::zero(), + "dissolving pool should not have any stake in the staking pallet" + ); + + // This shouldn't fail, but if it does we don't really care. Remaining balance can consist + // of unclaimed pending commission, erroneous transfers to the reward account, etc. + let reward_pool_remaining = T::Currency::free_balance(&reward_account); + + let _ = T::Currency::transfer( + &reward_account, + &bonded_pool.roles.depositor, + reward_pool_remaining, + ExistenceRequirement::AllowDeath, + ); + + defensive_assert!( + T::Currency::total_balance(&reward_account) == Zero::zero(), + "could not transfer all amount to depositor while dissolving pool" + ); + defensive_assert!( + T::Currency::total_balance(&bonded_pool.bonded_account()) == Zero::zero(), + "dissolving pool should not have any balance" + ); + // NOTE: Defensively force set balance to zero. + T::Currency::make_free_balance_be(&reward_account, Zero::zero()); + T::Currency::make_free_balance_be(&bonded_pool.bonded_account(), Zero::zero()); + + Self::deposit_event(Event::::Destroyed { pool_id: bonded_pool.id }); + // Remove bonded pool metadata. + Metadata::::remove(bonded_pool.id); + + bonded_pool.remove(); + } + + /// Create the main, bonded account of a pool with the given id. + pub fn create_bonded_account(id: PoolId) -> T::AccountId { + T::PalletId::get().into_sub_account_truncating((AccountType::Bonded, id)) + } + + /// Create the reward account of a pool with the given id. + pub fn create_reward_account(id: PoolId) -> T::AccountId { + // NOTE: in order to have a distinction in the test account id type (u128), we put + // account_type first so it does not get truncated out. + T::PalletId::get().into_sub_account_truncating((AccountType::Reward, id)) + } + + /// Calculate the equivalent point of `new_funds` in a pool with `current_balance` and + /// `current_points`. + fn balance_to_point( + current_balance: BalanceOf, + current_points: BalanceOf, + new_funds: BalanceOf, + ) -> BalanceOf { + let u256 = T::BalanceToU256::convert; + let balance = T::U256ToBalance::convert; + match (current_balance.is_zero(), current_points.is_zero()) { + (_, true) => new_funds.saturating_mul(POINTS_TO_BALANCE_INIT_RATIO.into()), + (true, false) => { + // The pool was totally slashed. + // This is the equivalent of `(current_points / 1) * new_funds`. + new_funds.saturating_mul(current_points) + }, + (false, false) => { + // Equivalent to (current_points / current_balance) * new_funds + balance( + u256(current_points) + .saturating_mul(u256(new_funds)) + // We check for zero above + .div(u256(current_balance)), + ) + }, + } + } + + /// Calculate the equivalent balance of `points` in a pool with `current_balance` and + /// `current_points`. + fn point_to_balance( + current_balance: BalanceOf, + current_points: BalanceOf, + points: BalanceOf, + ) -> BalanceOf { + let u256 = T::BalanceToU256::convert; + let balance = T::U256ToBalance::convert; + if current_balance.is_zero() || current_points.is_zero() || points.is_zero() { + // There is nothing to unbond + return Zero::zero(); + } + + // Equivalent of (current_balance / current_points) * points + balance( + u256(current_balance) + .saturating_mul(u256(points)) + // We check for zero above + .div(u256(current_points)), + ) + } + + fn do_create( + who: T::AccountId, + amount: BalanceOf, + root: AccountIdLookupOf, + nominator: AccountIdLookupOf, + bouncer: AccountIdLookupOf, + pool_id: PoolId, + ) -> DispatchResult { + let root = T::Lookup::lookup(root)?; + let nominator = T::Lookup::lookup(nominator)?; + let bouncer = T::Lookup::lookup(bouncer)?; + + // ensure that pool token can be created + // if this fails, it means that the pool token already exists or the token counter needs to be incremented correctly + ensure!( + T::Fungibles::total_issuance(pool_id.into()) == 0_u32.into(), + Error::::PoolTokenCreationFailed + ); + + let admin_account = T::PalletId::get().into_account_truncating(); + T::Fungibles::create(pool_id.into(), admin_account, false, 1_u32.into())?; + + ensure!(amount >= Pallet::::depositor_min_bond(), Error::::MinimumBondNotMet); + ensure!( + MaxPools::::get().map_or(true, |max_pools| BondedPools::::count() < max_pools), + Error::::MaxPools + ); + let mut bonded_pool = BondedPool::::new( + pool_id, + PoolRoles { + root: Some(root), + nominator: Some(nominator), + bouncer: Some(bouncer), + depositor: who.clone(), + }, + ); + + bonded_pool.try_bond_funds(&who, amount, BondType::Create)?; + + // Transfer the minimum balance for the reward account. + T::Currency::transfer( + &who, + &bonded_pool.reward_account(), + T::Currency::minimum_balance(), + ExistenceRequirement::KeepAlive, + )?; + + RewardPools::::insert( + pool_id, + RewardPool:: { + last_recorded_reward_counter: Zero::zero(), + last_recorded_total_payouts: Zero::zero(), + total_rewards_claimed: Zero::zero(), + total_commission_pending: Zero::zero(), + total_commission_claimed: Zero::zero(), + }, + ); + ReversePoolIdLookup::::insert(bonded_pool.bonded_account(), pool_id); + + Self::deposit_event(Event::::Created { depositor: who.clone(), pool_id }); + + Self::deposit_event(Event::::Bonded { + member: who, + pool_id, + bonded: amount, + joined: true, + }); + + bonded_pool.put(); + + Ok(()) + } + + fn do_bond_extra( + signer: T::AccountId, + member_account: T::AccountId, + pool_id: PoolId, + extra: BondExtra>, + ) -> DispatchResult { + if signer != member_account { + ensure!( + ClaimPermissions::::get(&member_account).can_bond_extra(), + Error::::DoesNotHavePermission + ); + } + + let mut bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + bonded_pool.ok_to_join()?; + + let (_points_issued, bonded) = match extra { + BondExtra::FreeBalance(amount) => { + (bonded_pool.try_bond_funds(&member_account, amount, BondType::Later)?, amount) + }, + }; + + bonded_pool.ok_to_be_open()?; + + Self::deposit_event(Event::::Bonded { + member: member_account.clone(), + pool_id, + bonded, + joined: false, + }); + + Ok(()) + } + + fn do_claim_commission(who: T::AccountId, pool_id: PoolId) -> DispatchResult { + let bonded_pool = BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?; + ensure!(bonded_pool.can_claim_commission(&who), Error::::DoesNotHavePermission); + + let mut reward_pool = RewardPools::::get(pool_id) + .defensive_ok_or::>(DefensiveError::RewardPoolNotFound.into())?; + + // IMPORTANT: ensure newly pending commission not yet processed is added to + // `total_commission_pending`. + reward_pool.update_records( + pool_id, + bonded_pool.points(), + bonded_pool.commission.current(), + )?; + + let commission = reward_pool.total_commission_pending; + ensure!(!commission.is_zero(), Error::::NoPendingCommission); + + let payee = bonded_pool + .commission + .current + .as_ref() + .map(|(_, p)| p.clone()) + .ok_or(Error::::NoCommissionCurrentSet)?; + + // Payout claimed commission. + T::Currency::transfer( + &bonded_pool.reward_account(), + &payee, + commission, + ExistenceRequirement::KeepAlive, + )?; + + // Add pending commission to total claimed counter. + reward_pool.total_commission_claimed = + reward_pool.total_commission_claimed.saturating_add(commission); + // Reset total pending commission counter to zero. + reward_pool.total_commission_pending = Zero::zero(); + RewardPools::::insert(pool_id, reward_pool); + + Self::deposit_event(Event::::PoolCommissionClaimed { pool_id, commission }); + Ok(()) + } + + fn do_adjust_pool_deposit(who: T::AccountId, pool: PoolId) -> DispatchResult { + let bonded_pool = BondedPool::::get(pool).ok_or(Error::::PoolNotFound)?; + let reward_acc = &bonded_pool.reward_account(); + let pre_frozen_balance = T::Currency::reserved_balance(reward_acc); + let min_balance = T::Currency::minimum_balance(); + + if pre_frozen_balance == min_balance { + return Err(Error::::NothingToAdjust.into()); + } + + // Update frozen amount with current ED. + Self::freeze_pool_deposit(reward_acc)?; + + if pre_frozen_balance > min_balance { + // Transfer excess back to depositor. + let excess = pre_frozen_balance.saturating_sub(min_balance); + T::Currency::transfer(reward_acc, &who, excess, ExistenceRequirement::KeepAlive)?; + Self::deposit_event(Event::::MinBalanceExcessAdjusted { + pool_id: pool, + amount: excess, + }); + } else { + // Transfer ED deficit from depositor to the pool + let deficit = min_balance.saturating_sub(pre_frozen_balance); + T::Currency::transfer(&who, reward_acc, deficit, ExistenceRequirement::KeepAlive)?; + Self::deposit_event(Event::::MinBalanceDeficitAdjusted { + pool_id: pool, + amount: deficit, + }); + } + + Ok(()) + } + + /// Apply freeze on reward account to restrict it from going below ED. + pub(crate) fn freeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult { + T::Currency::reserve(reward_acc, T::Currency::minimum_balance()) + } + + /// Removes the ED freeze on the reward account of `pool_id`. + pub fn unfreeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult { + let _ = T::Currency::unreserve(reward_acc, T::Currency::minimum_balance()); + Ok(()) + } + + /// Fully unbond the shares of `member`, when executed from `origin`. + /// + /// This is useful for backwards compatibility with the majority of tests that only deal with + /// full unbonding, not partial unbonding. + #[cfg(any(feature = "runtime-benchmarks", test))] + pub fn fully_unbond( + origin: frame_system::pallet_prelude::OriginFor, + member: T::AccountId, + pool_id: PoolId, + ) -> DispatchResult { + let points = T::Fungibles::balance(pool_id.into(), &member); + let member_lookup = T::Lookup::unlookup(member); + Self::unbond(origin, member_lookup, pool_id, points) + } +} + +impl sp_staking::OnStakingUpdate> for Pallet { + /// Reduces the balances of the [`SubPools`], that belong to the pool involved in the + /// slash, to the amount that is defined in the `slashed_unlocking` field of + /// [`sp_staking::OnStakingUpdate::on_slash`] + /// + /// Emits the `PoolsSlashed` event. + fn on_slash( + pool_account: &T::AccountId, + // Bonded balance is always read directly from staking, therefore we don't need to update + // anything here. + slashed_bonded: BalanceOf, + slashed_unlocking: &BTreeMap>, + total_slashed: BalanceOf, + ) { + let Some(pool_id) = ReversePoolIdLookup::::get(pool_account) else { return }; + // As the slashed account belongs to a `BondedPool` the `TotalValueLocked` decreases and + // an event is emitted. + TotalValueLocked::::mutate(|tvl| { + tvl.defensive_saturating_reduce(total_slashed); + }); + + if let Some(mut sub_pools) = SubPoolsStorage::::get(pool_id) { + // set the reduced balance for each of the `SubPools` + slashed_unlocking.iter().for_each(|(era, slashed_balance)| { + if let Some(pool) = sub_pools.with_era.get_mut(era).defensive() { + pool.balance = *slashed_balance; + Self::deposit_event(Event::::UnbondingPoolSlashed { + era: *era, + pool_id, + balance: *slashed_balance, + }); + } + }); + SubPoolsStorage::::insert(pool_id, sub_pools); + } else if !slashed_unlocking.is_empty() { + defensive!("Expected SubPools were not found"); + } + Self::deposit_event(Event::::PoolSlashed { pool_id, balance: slashed_bonded }); + } +} diff --git a/pallets/tangle-lst/src/mock.rs b/pallets/tangle-lst/src/mock.rs new file mode 100644 index 000000000..cfaabc3b0 --- /dev/null +++ b/pallets/tangle-lst/src/mock.rs @@ -0,0 +1,516 @@ +// This file is part of Substrate. + +use super::*; +use crate::{self as pallet_lst}; +use frame_support::traits::AsEnsureOriginWithArg; +use frame_support::{assert_ok, derive_impl, parameter_types, PalletId}; +use frame_system::RawOrigin; +use sp_runtime::traits::ConstU128; +use sp_runtime::Perbill; +use sp_runtime::{BuildStorage, FixedU128}; +use sp_staking::{OnStakingUpdate, Stake}; + +pub type BlockNumber = u64; +pub type AccountId = u128; +pub type Balance = u128; +pub type RewardCounter = FixedU128; +pub type AssetId = u32; +// This sneaky little hack allows us to write code exactly as we would do in the pallet in the tests +// as well, e.g. `StorageItem::::get()`. +pub type T = Runtime; +pub type Currency = ::Currency; + +// Ext builder creates a pool with id 1. +pub fn default_bonded_account() -> AccountId { + Lst::create_bonded_account(1) +} + +// Ext builder creates a pool with id 1. +pub fn default_reward_account() -> AccountId { + Lst::create_reward_account(1) +} + +parameter_types! { + pub static MinJoinBondConfig: Balance = 2; + pub static CurrentEra: EraIndex = 0; + pub static BondingDuration: EraIndex = 3; + pub storage BondedBalanceMap: BTreeMap = Default::default(); + // map from a user to a vec of eras and amounts being unlocked in each era. + pub storage UnbondingBalanceMap: BTreeMap> = Default::default(); + #[derive(Clone, PartialEq)] + pub static MaxUnbonding: u32 = 8; + pub static StakingMinBond: Balance = 10; + pub storage Nominations: Option> = None; +} +pub struct StakingMock; + +impl StakingMock { + pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) { + let mut x = BondedBalanceMap::get(); + x.insert(who, bonded); + BondedBalanceMap::set(&x) + } + /// Mimics a slash towards a pool specified by `pool_id`. + /// This reduces the bonded balance of a pool by `amount` and calls [`Lst::on_slash`] to + /// enact changes in the nomination-pool pallet. + /// + /// Does not modify any [`SubPools`] of the pool as [`Default::default`] is passed for + /// `slashed_unlocking`. + pub fn slash_by(pool_id: PoolId, amount: Balance) { + let acc = Lst::create_bonded_account(pool_id); + let bonded = BondedBalanceMap::get(); + let pre_total = bonded.get(&acc).unwrap(); + Self::set_bonded_balance(acc, pre_total - amount); + Lst::on_slash(&acc, pre_total - amount, &Default::default(), amount); + } +} + +impl sp_staking::StakingInterface for StakingMock { + type Balance = Balance; + type AccountId = AccountId; + type CurrencyToVote = (); + + fn minimum_nominator_bond() -> Self::Balance { + StakingMinBond::get() + } + fn minimum_validator_bond() -> Self::Balance { + StakingMinBond::get() + } + + fn desired_validator_count() -> u32 { + unimplemented!("method currently not used in testing") + } + + fn current_era() -> EraIndex { + CurrentEra::get() + } + + fn bonding_duration() -> EraIndex { + BondingDuration::get() + } + + fn status( + _: &Self::AccountId, + ) -> Result, DispatchError> { + Nominations::get() + .map(sp_staking::StakerStatus::Nominator) + .ok_or(DispatchError::Other("NotStash")) + } + + #[allow(clippy::option_map_unit_fn)] + fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { + let mut x = BondedBalanceMap::get(); + x.get_mut(who).map(|v| *v += extra); + BondedBalanceMap::set(&x); + Ok(()) + } + + fn unbond(who: &Self::AccountId, amount: Self::Balance) -> DispatchResult { + let mut x = BondedBalanceMap::get(); + *x.get_mut(who).unwrap() = x.get_mut(who).unwrap().saturating_sub(amount); + BondedBalanceMap::set(&x); + + let era = Self::current_era(); + let unlocking_at = era + Self::bonding_duration(); + let mut y = UnbondingBalanceMap::get(); + y.entry(*who).or_default().push((unlocking_at, amount)); + UnbondingBalanceMap::set(&y); + Ok(()) + } + + fn chill(_: &Self::AccountId) -> sp_runtime::DispatchResult { + Ok(()) + } + + fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result { + let mut unbonding_map = UnbondingBalanceMap::get(); + let staker_map = unbonding_map.get_mut(&who).ok_or("Nothing to unbond")?; + + let current_era = Self::current_era(); + staker_map.retain(|(unlocking_at, _amount)| *unlocking_at > current_era); + + UnbondingBalanceMap::set(&unbonding_map); + Ok(UnbondingBalanceMap::get().is_empty() && BondedBalanceMap::get().is_empty()) + } + + fn bond(stash: &Self::AccountId, value: Self::Balance, _: &Self::AccountId) -> DispatchResult { + StakingMock::set_bonded_balance(*stash, value); + Ok(()) + } + + fn nominate(_: &Self::AccountId, nominations: Vec) -> DispatchResult { + Nominations::set(&Some(nominations)); + Ok(()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn nominations(_: &Self::AccountId) -> Option> { + Nominations::get() + } + + fn stash_by_ctrl(_controller: &Self::AccountId) -> Result { + unimplemented!("method currently not used in testing") + } + + fn stake(who: &Self::AccountId) -> Result, DispatchError> { + match (UnbondingBalanceMap::get().get(who), BondedBalanceMap::get().get(who).copied()) { + (None, None) => Err(DispatchError::Other("balance not found")), + (Some(v), None) => Ok(Stake { + total: v.iter().fold(0u128, |acc, &x| acc.saturating_add(x.1)), + active: 0, + }), + (None, Some(v)) => Ok(Stake { total: v, active: v }), + (Some(a), Some(b)) => Ok(Stake { + total: a.iter().fold(0u128, |acc, &x| acc.saturating_add(x.1)) + b, + active: b, + }), + } + } + + fn election_ongoing() -> bool { + unimplemented!("method currently not used in testing") + } + + fn force_unstake(_who: Self::AccountId) -> sp_runtime::DispatchResult { + unimplemented!("method currently not used in testing") + } + + fn is_exposed_in_era(_who: &Self::AccountId, _era: &EraIndex) -> bool { + unimplemented!("method currently not used in testing") + } + + #[cfg(feature = "runtime-benchmarks")] + fn add_era_stakers( + _current_era: &EraIndex, + _stash: &Self::AccountId, + _exposures: Vec<(Self::AccountId, Self::Balance)>, + ) { + unimplemented!("method currently not used in testing") + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_current_era(_era: EraIndex) { + unimplemented!("method currently not used in testing") + } + + #[cfg(feature = "runtime-benchmarks")] + fn max_exposure_page_size() -> sp_staking::Page { + unimplemented!("method currently not used in testing") + } +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type SS58Prefix = (); + type BaseCallFilter = frame_support::traits::Everything; + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = sp_runtime::traits::IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type DbWeight = (); + type BlockLength = (); + type BlockWeights = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub static ExistentialDeposit: Balance = 5; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = frame_support::traits::ConstU32<1024>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = ConstU32<1>; + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); +} + +pub struct BalanceToU256; +impl Convert for BalanceToU256 { + fn convert(n: Balance) -> U256 { + n.into() + } +} + +pub struct U256ToBalance; +impl Convert for U256ToBalance { + fn convert(n: U256) -> Balance { + n.try_into().unwrap() + } +} + +parameter_types! { + pub static PostUnbondingPoolsWindow: u32 = 2; + pub static MaxMetadataLen: u32 = 2; + pub static CheckLevel: u8 = 255; + pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls"); +} + +impl pallet_lst::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RewardCounter = RewardCounter; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = StakingMock; + type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow; + type PalletId = PoolsPalletId; + type MaxMetadataLen = MaxMetadataLen; + type MaxUnbonding = MaxUnbonding; + type Fungibles = Assets; + type AssetId = AssetId; + type PoolId = PoolId; + type ForceOrigin = frame_system::EnsureRoot; + type MaxPointsToBalance = frame_support::traits::ConstU8<10>; +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type AssetId = AssetId; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = (); + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; +} + +type Block = frame_system::mocking::MockBlock; +frame_support::construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Assets: pallet_assets, + Lst: pallet_lst, + } +); + +pub struct ExtBuilder { + members: Vec<(AccountId, Balance)>, + max_members: Option, + max_members_per_pool: Option, + global_max_commission: Option, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + members: Default::default(), + max_members: Some(4), + max_members_per_pool: Some(3), + global_max_commission: Some(Perbill::from_percent(90)), + } + } +} + +#[cfg_attr(feature = "fuzzing", allow(dead_code))] +impl ExtBuilder { + // Add members to pool 0. + pub fn add_members(mut self, members: Vec<(AccountId, Balance)>) -> Self { + self.members = members; + self + } + + pub fn ed(self, ed: Balance) -> Self { + ExistentialDeposit::set(ed); + self + } + + pub fn min_bond(self, min: Balance) -> Self { + StakingMinBond::set(min); + self + } + + pub fn min_join_bond(self, min: Balance) -> Self { + MinJoinBondConfig::set(min); + self + } + + pub fn with_check(self, level: u8) -> Self { + CheckLevel::set(level); + self + } + + pub fn max_members(mut self, max: Option) -> Self { + self.max_members = max; + self + } + + pub fn max_members_per_pool(mut self, max: Option) -> Self { + self.max_members_per_pool = max; + self + } + + pub fn global_max_commission(mut self, commission: Option) -> Self { + self.global_max_commission = commission; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut storage = + frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let _ = crate::GenesisConfig:: { + min_join_bond: MinJoinBondConfig::get(), + min_create_bond: 2, + max_pools: Some(2), + max_members_per_pool: self.max_members_per_pool, + max_members: self.max_members, + global_max_commission: self.global_max_commission, + } + .assimilate_storage(&mut storage); + + let mut ext = sp_io::TestExternalities::from(storage); + + ext.execute_with(|| { + use frame_support::traits::Currency; + // for events to be deposited. + frame_system::Pallet::::set_block_number(1); + + // make a pool + let amount_to_bond = Lst::depositor_min_bond(); + ::Currency::make_free_balance_be(&10u32.into(), amount_to_bond * 5); + assert_ok!(Lst::create(RawOrigin::Signed(10).into(), amount_to_bond, 900, 901, 902)); + assert_ok!(Lst::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); + let last_pool = LastPoolId::::get(); + for (account_id, bonded) in self.members { + ::Currency::make_free_balance_be(&account_id, bonded * 2); + assert_ok!(Lst::join(RawOrigin::Signed(account_id).into(), bonded, last_pool)); + } + }); + + ext + } + + pub fn build_and_execute(self, test: impl FnOnce()) { + self.build().execute_with(|| { + test(); + //Pools::do_try_state(CheckLevel::get()).unwrap(); + }) + } +} + +pub fn unsafe_set_state(pool_id: PoolId, state: PoolState) { + BondedPools::::try_mutate(pool_id, |maybe_bonded_pool| { + maybe_bonded_pool.as_mut().ok_or(()).map(|bonded_pool| { + bonded_pool.state = state; + }) + }) + .unwrap() +} + +parameter_types! { + storage PoolsEvents: u32 = 0; + storage BalancesEvents: u32 = 0; +} + +/// Helper to run a specified amount of blocks. +pub fn run_blocks(n: u64) { + let current_block = System::block_number(); + run_to_block(n + current_block); +} + +/// Helper to run to a specific block. +pub fn run_to_block(n: u64) { + let current_block = System::block_number(); + assert!(n > current_block); + while System::block_number() < n { + Lst::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Lst::on_initialize(System::block_number()); + } +} + +/// All events of this pallet. +pub fn pool_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Lst(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = PoolsEvents::get(); + PoolsEvents::set(&(events.len() as u32)); + events.into_iter().skip(already_seen as usize).collect() +} + +/// All events of the `Balances` pallet. +pub fn balances_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Balances(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = BalancesEvents::get(); + BalancesEvents::set(&(events.len() as u32)); + events.into_iter().skip(already_seen as usize).collect() +} + +/// Same as `fully_unbond`, in permissioned setting. +pub fn fully_unbond_permissioned(pool_id: PoolId, member: AccountId) -> DispatchResult { + let points = Assets::balance(pool_id, member); + Lst::unbond(RuntimeOrigin::signed(member), member, pool_id, points) +} + +#[derive(PartialEq, Debug)] +pub enum RewardImbalance { + // There is no reward deficit. + Surplus(Balance), + // There is a reward deficit. + Deficit(Balance), +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn u256_to_balance_convert_works() { + assert_eq!(U256ToBalance::convert(0u32.into()), Zero::zero()); + assert_eq!(U256ToBalance::convert(Balance::MAX.into()), Balance::MAX) + } + + #[test] + #[should_panic] + fn u256_to_balance_convert_panics_correctly() { + U256ToBalance::convert(U256::from(Balance::MAX).saturating_add(1u32.into())); + } + + #[test] + fn balance_to_u256_convert_works() { + assert_eq!(BalanceToU256::convert(0u32.into()), U256::zero()); + assert_eq!(BalanceToU256::convert(Balance::MAX), Balance::MAX.into()) + } +} diff --git a/pallets/tangle-lst/src/tests.rs b/pallets/tangle-lst/src/tests.rs new file mode 100644 index 000000000..32bcdd442 --- /dev/null +++ b/pallets/tangle-lst/src/tests.rs @@ -0,0 +1,99 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +use super::*; +use crate::{mock::Currency, mock::*, Event}; +use frame_support::traits::Currency as CurrencyT; + +mod bond_extra; +mod bonded_pool; +mod create; +mod join; +mod slash; +mod sub_pools; +mod update_roles; + +pub const DEFAULT_ROLES: PoolRoles = + PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), bouncer: Some(902) }; + +fn mint_lst(pool_id: u32, who: &AccountId, amount: u128) { + if Assets::asset_exists(pool_id) { + Assets::mint_into(pool_id, who, amount).unwrap(); + } else { + Balances::make_free_balance_be(who, 10000); + Assets::force_create(RuntimeOrigin::root(), pool_id, *who, false, 1_u32.into()).unwrap(); + Assets::mint_into(pool_id, who, amount).unwrap(); + } +} + +fn burn_lst(pool_id: u32, who: &AccountId, amount: u128) { + Assets::burn_from(pool_id, who, amount, Precision::Exact, Fortitude::Force).unwrap(); +} + +#[test] +fn test_setup_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(BondedPools::::count(), 1); + assert_eq!(RewardPools::::count(), 1); + assert_eq!(SubPoolsStorage::::count(), 0); + assert_eq!(UnbondingMembers::::count(), 0); + assert_eq!(StakingMock::bonding_duration(), 3); + assert!(Metadata::::contains_key(1)); + + // initial member. + assert_eq!(TotalValueLocked::::get(), 10); + + let last_pool = LastPoolId::::get(); + assert_eq!( + BondedPool::::get(last_pool).unwrap(), + BondedPool:: { + id: last_pool, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + ); + assert_eq!( + RewardPools::::get(last_pool).unwrap(), + RewardPool:: { + last_recorded_reward_counter: Zero::zero(), + last_recorded_total_payouts: 0, + total_rewards_claimed: 0, + total_commission_claimed: 0, + total_commission_pending: 0, + } + ); + + let bonded_account = Lst::create_bonded_account(last_pool); + let reward_account = Lst::create_reward_account(last_pool); + + // the bonded_account should be bonded by the depositor's funds. + assert_eq!(StakingMock::active_stake(&bonded_account).unwrap(), 10); + assert_eq!(StakingMock::total_stake(&bonded_account).unwrap(), 10); + + // but not nominating yet. + assert!(Nominations::get().is_none()); + + // reward account should have an initial ED in it. + assert_eq!( + Currency::free_balance(reward_account), + >::minimum_balance() + ); + }) +} diff --git a/pallets/tangle-lst/src/tests/bond_extra.rs b/pallets/tangle-lst/src/tests/bond_extra.rs new file mode 100644 index 000000000..01bf4371b --- /dev/null +++ b/pallets/tangle-lst/src/tests/bond_extra.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::Event; +use frame_support::assert_ok; + +#[test] +fn bond_extra_from_free_balance_creator() { + ExtBuilder::default().build_and_execute(|| { + // 10 is the owner and a member in pool 1, give them some more funds. + Currency::make_free_balance_be(&10, 100); + + // given + assert_eq!(Currency::free_balance(10), 100); + + // when + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(10))); + + // then + assert_eq!(Currency::free_balance(10), 90); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false } + ] + ); + + // when + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(20))); + + // then + assert_eq!(Currency::free_balance(10), 70); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::Bonded { member: 10, pool_id: 1, bonded: 20, joined: false }] + ); + }) +} diff --git a/pallets/tangle-lst/src/tests/bonded_pool.rs b/pallets/tangle-lst/src/tests/bonded_pool.rs new file mode 100644 index 000000000..b833c55c1 --- /dev/null +++ b/pallets/tangle-lst/src/tests/bonded_pool.rs @@ -0,0 +1,204 @@ +use super::*; +use crate::mock::Currency; +use frame_support::traits::Currency as CurrencyT; +use frame_support::{assert_noop, assert_ok}; + +#[test] +fn test_setup_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(BondedPools::::count(), 1); + assert_eq!(RewardPools::::count(), 1); + assert_eq!(SubPoolsStorage::::count(), 0); + assert_eq!(UnbondingMembers::::count(), 0); + assert_eq!(StakingMock::bonding_duration(), 3); + assert!(Metadata::::contains_key(1)); + + // initial member. + assert_eq!(TotalValueLocked::::get(), 10); + + let last_pool = LastPoolId::::get(); + assert_eq!( + BondedPool::::get(last_pool).unwrap(), + BondedPool:: { + id: last_pool, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + ); + assert_eq!( + RewardPools::::get(last_pool).unwrap(), + RewardPool:: { + last_recorded_reward_counter: Zero::zero(), + last_recorded_total_payouts: 0, + total_rewards_claimed: 0, + total_commission_claimed: 0, + total_commission_pending: 0, + } + ); + + let bonded_account = Lst::create_bonded_account(last_pool); + let reward_account = Lst::create_reward_account(last_pool); + + // the bonded_account should be bonded by the depositor's funds. + assert_eq!(StakingMock::active_stake(&bonded_account).unwrap(), 10); + assert_eq!(StakingMock::total_stake(&bonded_account).unwrap(), 10); + + // but not nominating yet. + assert!(Nominations::get().is_none()); + + // reward account should have an initial ED in it. + assert_eq!( + Currency::free_balance(reward_account), + >::minimum_balance() + ); + }) +} + +#[test] +fn balance_to_point_works() { + ExtBuilder::default().build_and_execute(|| { + let bonded_pool = BondedPool:: { + id: 123123, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + + // 1 points : 1 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.balance_to_point(10), 10); + assert_eq!(bonded_pool.balance_to_point(0), 0); + + // 2 points : 1 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 50); + assert_eq!(bonded_pool.balance_to_point(10), 20); + + // 1 points : 2 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + burn_lst(bonded_pool.id, &bonded_pool.bonded_account(), 50); + assert_eq!(bonded_pool.balance_to_point(10), 5); + + // 100 points : 0 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 0); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 50); + assert_eq!(bonded_pool.balance_to_point(10), 100 * 10); + + // 0 points : 100 balance + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + burn_lst(bonded_pool.id, &bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.balance_to_point(10), 10); + + // 10 points : 3 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 30); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.balance_to_point(10), 33); + + // 2 points : 3 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 300); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.balance_to_point(10), 6); + + // 4 points : 9 balance ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 900); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 200); + assert_eq!(bonded_pool.balance_to_point(90), 40); + }) +} + +#[test] +fn points_to_balance_works() { + ExtBuilder::default().build_and_execute(|| { + // 1 balance : 1 points ratio + let bonded_pool = BondedPool:: { + id: 123123, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 100); + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.points_to_balance(10), 10); + assert_eq!(bonded_pool.points_to_balance(0), 0); + + // 2 balance : 1 points ratio + burn_lst(bonded_pool.id, &bonded_pool.bonded_account(), 50); + assert_eq!(bonded_pool.points_to_balance(10), 20); + + // 100 balance : 0 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + burn_lst(bonded_pool.id, &bonded_pool.bonded_account(), 50); + assert_eq!(bonded_pool.points_to_balance(10), 0); + + // 0 balance : 100 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 0); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 100); + assert_eq!(bonded_pool.points_to_balance(10), 0); + + // 10 balance : 3 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); + burn_lst(bonded_pool.id, &bonded_pool.bonded_account(), 70); + assert_eq!(bonded_pool.points_to_balance(10), 33); + + // 2 balance : 3 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 200); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 270); + assert_eq!(bonded_pool.points_to_balance(10), 6); + + // 4 balance : 9 points ratio + StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 400); + mint_lst(bonded_pool.id, &bonded_pool.bonded_account(), 600); + assert_eq!(bonded_pool.points_to_balance(90), 40); + }) +} + +#[test] +fn ok_to_join_with_works() { + ExtBuilder::default().build_and_execute(|| { + let pool = BondedPool:: { + id: 123, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + + let max_points_to_balance: u128 = + <::MaxPointsToBalance as Get>::get().into(); + + // Simulate a 100% slashed pool + StakingMock::set_bonded_balance(pool.bonded_account(), 0); + assert_noop!(pool.ok_to_join(), Error::::OverflowRisk); + + // Simulate a slashed pool at `MaxPointsToBalance` + 1 slashed pool + StakingMock::set_bonded_balance( + pool.bonded_account(), + max_points_to_balance.saturating_add(1), + ); + assert_ok!(pool.ok_to_join()); + + // Simulate a slashed pool at `MaxPointsToBalance` + StakingMock::set_bonded_balance(pool.bonded_account(), max_points_to_balance); + + StakingMock::set_bonded_balance( + pool.bonded_account(), + Balance::MAX / max_points_to_balance, + ); + + // and a sanity check + StakingMock::set_bonded_balance( + pool.bonded_account(), + Balance::MAX / max_points_to_balance - 1, + ); + assert_ok!(pool.ok_to_join()); + }); +} diff --git a/pallets/tangle-lst/src/tests/create.rs b/pallets/tangle-lst/src/tests/create.rs new file mode 100644 index 000000000..58ee8a105 --- /dev/null +++ b/pallets/tangle-lst/src/tests/create.rs @@ -0,0 +1,137 @@ +use super::*; +use frame_support::assert_err; +use frame_support::assert_noop; +use frame_support::assert_ok; + +#[test] +fn create_works() { + ExtBuilder::default().build_and_execute(|| { + // next pool id is 2. + let next_pool_stash = Lst::create_bonded_account(2); + let ed = >::minimum_balance(); + + assert_eq!(TotalValueLocked::::get(), 10); + assert!(!BondedPools::::contains_key(2)); + assert!(!RewardPools::::contains_key(2)); + assert_err!(StakingMock::active_stake(&next_pool_stash), "balance not found"); + + Currency::make_free_balance_be(&11, StakingMock::minimum_nominator_bond() * 10 + ed); + assert_ok!(Lst::create( + RuntimeOrigin::signed(11), + StakingMock::minimum_nominator_bond(), + 123, + 456, + 789 + )); + assert_eq!(TotalValueLocked::::get(), 10 + StakingMock::minimum_nominator_bond()); + + assert_eq!(Currency::free_balance(11), 90); + assert_eq!( + BondedPool::::get(2).unwrap(), + BondedPool { + id: 2, + inner: BondedPoolInner { + commission: Commission::default(), + roles: PoolRoles { + depositor: 11, + root: Some(123), + nominator: Some(456), + bouncer: Some(789) + }, + state: PoolState::Open, + } + } + ); + assert_eq!( + StakingMock::active_stake(&next_pool_stash).unwrap(), + StakingMock::minimum_nominator_bond() + ); + assert_eq!(RewardPools::::get(2).unwrap(), RewardPool { ..Default::default() }); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Created { depositor: 11, pool_id: 2 }, + Event::Bonded { member: 11, pool_id: 2, bonded: 10, joined: true } + ] + ); + }); +} + +#[test] +fn create_errors_correctly() { + ExtBuilder::default().with_check(0).build_and_execute(|| { + // Given + assert_eq!(MinCreateBond::::get(), 2); + assert_eq!(StakingMock::minimum_nominator_bond(), 10); + + // Then + assert_noop!( + Lst::create(RuntimeOrigin::signed(11), 9, 123, 456, 789), + Error::::MinimumBondNotMet + ); + + // Given + MinCreateBond::::put(20); + + // Then + assert_noop!( + Lst::create(RuntimeOrigin::signed(11), 19, 123, 456, 789), + Error::::MinimumBondNotMet + ); + + // Given + BondedPool:: { + id: 2, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + assert_eq!(MaxPools::::get(), Some(2)); + assert_eq!(BondedPools::::count(), 2); + + // Then + assert_noop!( + Lst::create(RuntimeOrigin::signed(11), 20, 123, 456, 789), + Error::::MaxPools + ); + }); +} + +#[test] +fn create_with_pool_id_works() { + ExtBuilder::default().build_and_execute(|| { + let ed = >::minimum_balance(); + + Currency::make_free_balance_be(&11, StakingMock::minimum_nominator_bond() * 10 + ed); + assert_ok!(Lst::create( + RuntimeOrigin::signed(11), + StakingMock::minimum_nominator_bond(), + 123, + 456, + 789 + )); + + assert_eq!(Currency::free_balance(11), 90); + // delete the initial pool created, then pool_Id `1` will be free + + assert_noop!( + Lst::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 1), + Error::::PoolIdInUse + ); + + assert_noop!( + Lst::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 3), + Error::::InvalidPoolId + ); + + // start dismantling the pool. + assert_ok!(Lst::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); + assert_ok!(fully_unbond_permissioned(10, 1)); + }); +} diff --git a/pallets/tangle-lst/src/tests/join.rs b/pallets/tangle-lst/src/tests/join.rs new file mode 100644 index 000000000..804b83263 --- /dev/null +++ b/pallets/tangle-lst/src/tests/join.rs @@ -0,0 +1,152 @@ +use super::*; +use crate::{mock::Currency, Event}; +use frame_support::{assert_noop, assert_ok}; + +#[test] +fn join_works() { + let bonded = |_points| BondedPool:: { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + ExtBuilder::default().with_check(0).build_and_execute(|| { + // Given + Currency::make_free_balance_be(&11, ExistentialDeposit::get() + 2); + assert_eq!(TotalValueLocked::::get(), 10); + + // When + assert_ok!(Lst::join(RuntimeOrigin::signed(11), 2, 1)); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, + ] + ); + assert_eq!(TotalValueLocked::::get(), 12); + + assert_eq!(Assets::balance(1, 11), 2); + + assert_eq!(BondedPool::::get(1).unwrap(), bonded(12)); + + // Given + // The bonded balance is slashed in half + StakingMock::slash_by(1, 6); + + // And + Currency::make_free_balance_be(&12, ExistentialDeposit::get() + 12); + + // When + assert_ok!(Lst::join(RuntimeOrigin::signed(12), 12, 1)); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolSlashed { pool_id: 1, balance: 6 }, + Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true } + ] + ); + assert_eq!(TotalValueLocked::::get(), 18); + + assert_eq!(BondedPool::::get(1).unwrap(), bonded(12 + 24)); + }); +} + +#[test] +fn join_errors_correctly() { + ExtBuilder::default().with_check(0).build_and_execute(|| { + assert_noop!( + Lst::join(RuntimeOrigin::signed(11), 420, 123), + Error::::PoolNotFound + ); + + // Force the pools bonded balance to 0, simulating a 100% slash + StakingMock::set_bonded_balance(Lst::create_bonded_account(1), 0); + assert_noop!(Lst::join(RuntimeOrigin::signed(11), 420, 1), Error::::OverflowRisk); + + // Given a mocked bonded pool + BondedPool:: { + id: 123, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + + // and reward pool + RewardPools::::insert(123, RewardPool:: { ..Default::default() }); + + // Force the points:balance ratio to `MaxPointsToBalance` (100/10) + let max_points_to_balance: u128 = + <::MaxPointsToBalance as Get>::get().into(); + + StakingMock::set_bonded_balance(Lst::create_bonded_account(123), max_points_to_balance); + assert_noop!( + Lst::join(RuntimeOrigin::signed(11), 420, 123), + Error::::OverflowRisk + ); + + StakingMock::set_bonded_balance( + Lst::create_bonded_account(123), + Balance::MAX / max_points_to_balance, + ); + + // // Balance needs to be gt Balance::MAX / `MaxPointsToBalance` + // assert_noop!( + // Lst::join(RuntimeOrigin::signed(11), 5, 123), + // TokenError::FundsUnavailable, + // ); + + StakingMock::set_bonded_balance(Lst::create_bonded_account(1), max_points_to_balance); + + // Cannot join a pool that isn't open + unsafe_set_state(123, PoolState::Blocked); + assert_noop!( + Lst::join(RuntimeOrigin::signed(11), max_points_to_balance, 123), + Error::::NotOpen + ); + + unsafe_set_state(123, PoolState::Destroying); + assert_noop!( + Lst::join(RuntimeOrigin::signed(11), max_points_to_balance, 123), + Error::::NotOpen + ); + + // Given + MinJoinBond::::put(100); + + // Then + assert_noop!( + Lst::join(RuntimeOrigin::signed(11), 99, 123), + Error::::MinimumBondNotMet + ); + }); +} + +#[test] +#[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] +#[cfg_attr(not(debug_assertions), should_panic)] +fn join_panics_when_reward_pool_not_found() { + ExtBuilder::default().build_and_execute(|| { + StakingMock::set_bonded_balance(Lst::create_bonded_account(123), 100); + BondedPool:: { + id: 123, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + } + .put(); + let _ = Lst::join(RuntimeOrigin::signed(11), 420, 123); + }); +} diff --git a/pallets/tangle-lst/src/tests/nominate.rs b/pallets/tangle-lst/src/tests/nominate.rs new file mode 100644 index 000000000..8177a104b --- /dev/null +++ b/pallets/tangle-lst/src/tests/nominate.rs @@ -0,0 +1,155 @@ +use super::*; +use frame_support::assert_err; +use frame_support::assert_noop; +use frame_support::assert_ok; +use frame_support::traits::fungible::InspectFreeze; + + + #[test] + fn nominate_works() { + ExtBuilder::default().build_and_execute(|| { + // Depositor can't nominate + assert_noop!( + Lst::nominate(RuntimeOrigin::signed(10), 1, vec![21]), + Error::::NotNominator + ); + + // bouncer can't nominate + assert_noop!( + Lst::nominate(RuntimeOrigin::signed(902), 1, vec![21]), + Error::::NotNominator + ); + + // Root can nominate + assert_ok!(Lst::nominate(RuntimeOrigin::signed(900), 1, vec![21])); + assert_eq!(Nominations::get().unwrap(), vec![21]); + + // Nominator can nominate + assert_ok!(Lst::nominate(RuntimeOrigin::signed(901), 1, vec![31])); + assert_eq!(Nominations::get().unwrap(), vec![31]); + + // Can't nominate for a pool that doesn't exist + assert_noop!( + Lst::nominate(RuntimeOrigin::signed(902), 123, vec![21]), + Error::::PoolNotFound + ); + }); + } + + + #[test] + fn set_state_works() { + ExtBuilder::default().build_and_execute(|| { + // Given + assert_ok!(BondedPool::::get(1).unwrap().ok_to_be_open()); + + // Only the root and bouncer can change the state when the pool is ok to be open. + assert_noop!( + Lst::set_state(RuntimeOrigin::signed(10), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + assert_noop!( + Lst::set_state(RuntimeOrigin::signed(901), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + + // Root can change state + assert_ok!(Lst::set_state(RuntimeOrigin::signed(900), 1, PoolState::Blocked)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::StateChanged { pool_id: 1, new_state: PoolState::Blocked } + ] + ); + + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Blocked); + + // bouncer can change state + assert_ok!(Lst::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); + + // If the pool is destroying, then no one can set state + assert_noop!( + Lst::set_state(RuntimeOrigin::signed(900), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + assert_noop!( + Lst::set_state(RuntimeOrigin::signed(902), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + + // If the pool is not ok to be open, then anyone can set it to destroying + + // Given + unsafe_set_state(1, PoolState::Open); + // slash the pool to the point that `max_points_to_balance` ratio is + // surpassed. Making this pool destroyable by anyone. + StakingMock::slash_by(1, 10); + + // When + assert_ok!(Lst::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying)); + // Then + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); + + // Given + Currency::make_free_balance_be(&default_bonded_account(), Balance::MAX / 10); + unsafe_set_state(1, PoolState::Open); + // When + assert_ok!(Lst::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying)); + // Then + assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); + + // If the pool is not ok to be open, it cannot be permissionlessly set to a state that + // isn't destroying + unsafe_set_state(1, PoolState::Open); + assert_noop!( + Lst::set_state(RuntimeOrigin::signed(11), 1, PoolState::Blocked), + Error::::CanNotChangeState + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + Event::PoolSlashed { pool_id: 1, balance: 0 }, + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying } + ] + ); + }); + } + + + #[test] + fn set_metadata_works() { + ExtBuilder::default().build_and_execute(|| { + // Root can set metadata + assert_ok!(Lst::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); + assert_eq!(Metadata::::get(1), vec![1, 1]); + + // bouncer can set metadata + assert_ok!(Lst::set_metadata(RuntimeOrigin::signed(902), 1, vec![2, 2])); + assert_eq!(Metadata::::get(1), vec![2, 2]); + + // Depositor can't set metadata + assert_noop!( + Lst::set_metadata(RuntimeOrigin::signed(10), 1, vec![3, 3]), + Error::::DoesNotHavePermission + ); + + // Nominator can't set metadata + assert_noop!( + Lst::set_metadata(RuntimeOrigin::signed(901), 1, vec![3, 3]), + Error::::DoesNotHavePermission + ); + + // Metadata cannot be longer than `MaxMetadataLen` + assert_noop!( + Lst::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1, 1]), + Error::::MetadataExceedsMaxLen + ); + }); + } diff --git a/pallets/tangle-lst/src/tests/slash.rs b/pallets/tangle-lst/src/tests/slash.rs new file mode 100644 index 000000000..f52f1d3af --- /dev/null +++ b/pallets/tangle-lst/src/tests/slash.rs @@ -0,0 +1,56 @@ +use super::*; +use frame_support::assert_ok; + +#[test] +fn slash_no_subpool_is_tracked() { + let bonded = |_points| BondedPool:: { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + }, + }; + ExtBuilder::default().with_check(0).build_and_execute(|| { + // Given + Currency::make_free_balance_be(&11, ExistentialDeposit::get() + 2); + assert_eq!(TotalValueLocked::::get(), 10); + + // When + assert_ok!(Lst::join(RuntimeOrigin::signed(11), 2, 1)); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, + ] + ); + assert_eq!(TotalValueLocked::::get(), 12); + + assert_eq!(BondedPool::::get(1).unwrap(), bonded(12)); + + // Given + // The bonded balance is slashed in half + StakingMock::slash_by(1, 6); + + // And + Currency::make_free_balance_be(&12, ExistentialDeposit::get() + 12); + + // When + assert_ok!(Lst::join(RuntimeOrigin::signed(12), 12, 1)); + + // Then + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PoolSlashed { pool_id: 1, balance: 6 }, + Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true } + ] + ); + assert_eq!(TotalValueLocked::::get(), 18); + assert_eq!(BondedPool::::get(1).unwrap(), bonded(12 + 24)); + }); +} diff --git a/pallets/tangle-lst/src/tests/sub_pools.rs b/pallets/tangle-lst/src/tests/sub_pools.rs new file mode 100644 index 000000000..090e379bd --- /dev/null +++ b/pallets/tangle-lst/src/tests/sub_pools.rs @@ -0,0 +1,153 @@ +use super::*; + +macro_rules! unbonding_pools_with_era { + ($($k:expr => $v:expr),* $(,)?) => {{ + use sp_std::iter::{Iterator, IntoIterator}; + let not_bounded: BTreeMap<_, _> = Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*])); + BoundedBTreeMap::, TotalUnbondingPools>::try_from(not_bounded).unwrap() + }}; +} + +#[test] +fn points_to_issue_works() { + ExtBuilder::default().build_and_execute(|| { + // 1 points : 1 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 100 }; + assert_eq!(unbond_pool.balance_to_point(10), 10); + assert_eq!(unbond_pool.balance_to_point(0), 0); + + // 2 points : 1 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 50 }; + assert_eq!(unbond_pool.balance_to_point(10), 20); + + // 1 points : 2 balance ratio + let unbond_pool = UnbondPool:: { points: 50, balance: 100 }; + assert_eq!(unbond_pool.balance_to_point(10), 5); + + // 100 points : 0 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 0 }; + assert_eq!(unbond_pool.balance_to_point(10), 100 * 10); + + // 0 points : 100 balance + let unbond_pool = UnbondPool:: { points: 0, balance: 100 }; + assert_eq!(unbond_pool.balance_to_point(10), 10); + + // 10 points : 3 balance ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 30 }; + assert_eq!(unbond_pool.balance_to_point(10), 33); + + // 2 points : 3 balance ratio + let unbond_pool = UnbondPool:: { points: 200, balance: 300 }; + assert_eq!(unbond_pool.balance_to_point(10), 6); + + // 4 points : 9 balance ratio + let unbond_pool = UnbondPool:: { points: 400, balance: 900 }; + assert_eq!(unbond_pool.balance_to_point(90), 40); + }) +} + +#[test] +fn balance_to_unbond_works() { + // 1 balance : 1 points ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 10); + assert_eq!(unbond_pool.point_to_balance(0), 0); + + // 1 balance : 2 points ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 50 }; + assert_eq!(unbond_pool.point_to_balance(10), 5); + + // 2 balance : 1 points ratio + let unbond_pool = UnbondPool:: { points: 50, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 20); + + // 100 balance : 0 points ratio + let unbond_pool = UnbondPool:: { points: 0, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 0); + + // 0 balance : 100 points ratio + let unbond_pool = UnbondPool:: { points: 100, balance: 0 }; + assert_eq!(unbond_pool.point_to_balance(10), 0); + + // 10 balance : 3 points ratio + let unbond_pool = UnbondPool:: { points: 30, balance: 100 }; + assert_eq!(unbond_pool.point_to_balance(10), 33); + + // 2 balance : 3 points ratio + let unbond_pool = UnbondPool:: { points: 300, balance: 200 }; + assert_eq!(unbond_pool.point_to_balance(10), 6); + + // 4 balance : 9 points ratio + let unbond_pool = UnbondPool:: { points: 900, balance: 400 }; + assert_eq!(unbond_pool.point_to_balance(90), 40); +} + +#[test] +fn maybe_merge_pools_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(TotalUnbondingPools::::get(), 5); + assert_eq!(BondingDuration::get(), 3); + assert_eq!(PostUnbondingPoolsWindow::get(), 2); + + // Given + let mut sub_pool_0 = SubPools:: { + no_era: UnbondPool::::default(), + with_era: unbonding_pools_with_era! { + 0 => UnbondPool:: { points: 10, balance: 10 }, + 1 => UnbondPool:: { points: 10, balance: 10 }, + 2 => UnbondPool:: { points: 20, balance: 20 }, + 3 => UnbondPool:: { points: 30, balance: 30 }, + 4 => UnbondPool:: { points: 40, balance: 40 }, + }, + }; + + // When `current_era < TotalUnbondingPools`, + let sub_pool_1 = sub_pool_0.clone().maybe_merge_pools(0); + + // Then it exits early without modifications + assert_eq!(sub_pool_1, sub_pool_0); + + // When `current_era == TotalUnbondingPools`, + let sub_pool_1 = sub_pool_1.maybe_merge_pools(1); + + // Then it exits early without modifications + assert_eq!(sub_pool_1, sub_pool_0); + + // When `current_era - TotalUnbondingPools == 0`, + let mut sub_pool_1 = sub_pool_1.maybe_merge_pools(2); + + // Then era 0 is merged into the `no_era` pool + sub_pool_0.no_era = sub_pool_0.with_era.remove(&0).unwrap(); + assert_eq!(sub_pool_1, sub_pool_0); + + // Given we have entries for era 1..=5 + sub_pool_1 + .with_era + .try_insert(5, UnbondPool:: { points: 50, balance: 50 }) + .unwrap(); + sub_pool_0 + .with_era + .try_insert(5, UnbondPool:: { points: 50, balance: 50 }) + .unwrap(); + + // When `current_era - TotalUnbondingPools == 1` + let sub_pool_2 = sub_pool_1.maybe_merge_pools(3); + let era_1_pool = sub_pool_0.with_era.remove(&1).unwrap(); + + // Then era 1 is merged into the `no_era` pool + sub_pool_0.no_era.points += era_1_pool.points; + sub_pool_0.no_era.balance += era_1_pool.balance; + assert_eq!(sub_pool_2, sub_pool_0); + + // When `current_era - TotalUnbondingPools == 5`, so all pools with era <= 4 are removed + let sub_pool_3 = sub_pool_2.maybe_merge_pools(7); + + // Then all eras <= 5 are merged into the `no_era` pool + for era in 2..=5 { + let to_merge = sub_pool_0.with_era.remove(&era).unwrap(); + sub_pool_0.no_era.points += to_merge.points; + sub_pool_0.no_era.balance += to_merge.balance; + } + assert_eq!(sub_pool_3, sub_pool_0); + }); +} diff --git a/pallets/tangle-lst/src/tests/unbond.rs b/pallets/tangle-lst/src/tests/unbond.rs new file mode 100644 index 000000000..5e1723446 --- /dev/null +++ b/pallets/tangle-lst/src/tests/unbond.rs @@ -0,0 +1,825 @@ +use super::*; +use crate::{mock::Currency, Event}; +use frame_support::traits::Currency as CurrencyT; +use frame_support::{assert_noop, assert_ok}; + +macro_rules! unbonding_pools_with_era { + ($($k:expr => $v:expr),* $(,)?) => {{ + use sp_std::iter::{Iterator, IntoIterator}; + let not_bounded: BTreeMap<_, _> = Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*])); + BoundedBTreeMap::, TotalUnbondingPools>::try_from(not_bounded).unwrap() + }}; +} + +macro_rules! member_unbonding_eras { + ($( $any:tt )*) => {{ + let x: BoundedBTreeMap = bounded_btree_map!($( $any )*); + x + }}; +} + +#[test] +fn member_unbond_open() { + // depositor in pool, pool state open + // - member unbond above limit + // - member unbonds to 0 + // - member cannot unbond between within limit and 0 + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + assert_eq!(TotalValueLocked::::get(), 30); + // can unbond to above limit + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 5)); + + // tvl remains unchanged. + assert_eq!(TotalValueLocked::::get(), 30); + + // cannot go to below 10: + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 10), + Error::::MinimumBondNotMet + ); + }) +} + +#[test] +fn member_kicked() { + // depositor in pool, pool state blocked + // - member cannot be kicked to above limit + // - member cannot be kicked between within limit and 0 + // - member kicked to 0 + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + unsafe_set_state(1, PoolState::Blocked); + let kicker = DEFAULT_ROLES.bouncer.unwrap(); + + // cannot be kicked to above the limit. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(kicker), 20, 1, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // cannot go to below 10: + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(kicker), 20, 1, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // but they themselves can do an unbond + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 2)); + + // can be kicked to 0. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(kicker), 20, 1, 18)); + }) +} + +#[test] +fn member_unbond_destroying() { + // depositor in pool, pool state destroying + // - member cannot be permissionlessly unbonded to above limit + // - member cannot be permissionlessly unbonded between within limit and 0 + // - member permissionlessly unbonded to 0 + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + unsafe_set_state(1, PoolState::Destroying); + let random = 123; + + // cannot be kicked to above the limit. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(random), 20, 1, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // cannot go to below 10: + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(random), 20, 1, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // but they themselves can do an unbond + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 2)); + + // but can go to 0 + assert_ok!(Lst::unbond(RuntimeOrigin::signed(random), 20, 1, 18)); + }) +} + +#[test] +fn depositor_unbond_open() { + // depositor in pool, pool state open + // - depositor unbonds to above limit + // - depositor cannot unbond to below limit or 0 + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(10))); + + // can unbond to above the limit. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 5)); + + // cannot go to below 10: + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 10), + Error::::MinimumBondNotMet + ); + + // cannot go to 0 either. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 15), + Error::::MinimumBondNotMet + ); + }) +} + +#[test] +fn depositor_kick() { + // depositor in pool, pool state blocked + // - depositor can never be kicked. + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(10))); + + // set the stage + unsafe_set_state(1, PoolState::Blocked); + let kicker = DEFAULT_ROLES.bouncer.unwrap(); + + // cannot be kicked to above limit. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(kicker), 10, 1, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or below the limit + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(kicker), 10, 1, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or 0. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(kicker), 10, 1, 20), + Error::::DoesNotHavePermission + ); + + // they themselves cannot do it either + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 20), + Error::::MinimumBondNotMet + ); + }) +} + +#[test] +fn depositor_unbond_destroying_permissionless() { + // depositor can never be permissionlessly unbonded. + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(10))); + + // set the stage + unsafe_set_state(1, PoolState::Destroying); + let random = 123; + + // cannot be kicked to above limit. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(random), 10, 1, 5), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or below the limit + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(random), 10, 1, 15), + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // or 0. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(random), 10, 1, 20), + Error::::DoesNotHavePermission + ); + + // they themselves can do it in this case though. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 20)); + }) +} + +#[test] +fn depositor_unbond_destroying_not_last_member() { + // deposit in pool, pool state destroying + // - depositor can never leave if there is another member in the pool. + ExtBuilder::default() + .min_join_bond(10) + .add_members(vec![(20, 20)]) + .build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(10))); + + // set the stage + unsafe_set_state(1, PoolState::Destroying); + + // can go above the limit + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 5)); + + // but not below the limit + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 10), + Error::::MinimumBondNotMet + ); + + // and certainly not zero + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 15), + Error::::MinimumBondNotMet + ); + }) +} + +#[test] +fn depositor_unbond_destroying_last_member() { + // deposit in pool, pool state destroying + // - depositor can unbond to above limit always. + // - depositor cannot unbond to below limit if last. + // - depositor can unbond to 0 if last and destroying. + ExtBuilder::default().min_join_bond(10).build_and_execute(|| { + // give the depositor some extra funds. + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), 1, BondExtra::FreeBalance(10))); + + // set the stage + unsafe_set_state(1, PoolState::Destroying); + + // can unbond to above the limit. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 5)); + + // still cannot go to below limit + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 10), + Error::::MinimumBondNotMet + ); + + // can go to 0 too. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 15)); + }) +} + +#[test] +fn unbond_of_1_works() { + ExtBuilder::default().build_and_execute(|| { + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(10, 1)); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool:: { points: 10, balance: 10 }} + ); + + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Destroying, + } + } + ); + + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); + }); +} + +#[test] +fn unbond_of_3_works() { + ExtBuilder::default() + .add_members(vec![(40, 40), (550, 550)]) + .build_and_execute(|| { + let ed = >::minimum_balance(); + // Given a slash from 600 -> 500 + StakingMock::slash_by(1, 500); + + // and unclaimed rewards of 600. + Currency::make_free_balance_be(&default_reward_account(), ed + 600); + + // When + assert_ok!(fully_unbond_permissioned(40, 1)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }} + ); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, + Event::PoolSlashed { pool_id: 1, balance: 100 }, + Event::PaidOut { member: 40, pool_id: 1, payout: 40 }, + Event::Unbonded { member: 40, pool_id: 1, balance: 6, points: 6, era: 3 } + ] + ); + + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 94); + assert_eq!(Currency::free_balance(40), 40 + 40); // We claim rewards when unbonding + + // When + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(550, 1)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 98, balance: 98 }} + ); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Destroying, + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 2); + assert_eq!(Currency::free_balance(550), 550 + 550); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::PaidOut { member: 550, pool_id: 1, payout: 550 }, + Event::Unbonded { member: 550, pool_id: 1, points: 92, balance: 92, era: 3 } + ] + ); + + // When + CurrentEra::set(3); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 40, 1, 0)); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 550, 1, 0)); + assert_ok!(fully_unbond_permissioned(10, 1)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 6 => UnbondPool { points: 2, balance: 2 }} + ); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Destroying, + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); + + assert_eq!(Currency::free_balance(550), 550 + 550 + 92); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 40, pool_id: 1, points: 6, balance: 6 }, + Event::MemberRemoved { pool_id: 1, member: 40 }, + Event::Withdrawn { member: 550, pool_id: 1, points: 92, balance: 92 }, + Event::MemberRemoved { pool_id: 1, member: 550 }, + Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, + Event::Unbonded { member: 10, pool_id: 1, points: 2, balance: 2, era: 6 } + ] + ); + }); +} + +#[test] +fn unbond_merges_older_pools() { + ExtBuilder::default().with_check(1).build_and_execute(|| { + // Given + assert_eq!(StakingMock::bonding_duration(), 3); + SubPoolsStorage::::insert( + 1, + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { balance: 10, points: 100 }, + 1 + 3 => UnbondPool { balance: 20, points: 20 }, + 2 + 3 => UnbondPool { balance: 101, points: 101} + }, + }, + ); + unsafe_set_state(1, PoolState::Destroying); + + // When + let current_era = 1 + TotalUnbondingPools::::get(); + CurrentEra::set(current_era); + + assert_ok!(fully_unbond_permissioned(10, 1)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: UnbondPool { balance: 10 + 20, points: 100 + 20 }, + with_era: unbonding_pools_with_era! { + 2 + 3 => UnbondPool { balance: 101, points: 101}, + current_era + 3 => UnbondPool { balance: 10, points: 10 }, + }, + }, + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 } + ] + ); + }); +} + +#[test] +fn unbond_kick_works() { + // Kick: the pool is blocked and the caller is either the root or bouncer. + ExtBuilder::default() + .add_members(vec![(100, 100), (200, 200)]) + .build_and_execute(|| { + // Given + unsafe_set_state(1, PoolState::Blocked); + let bonded_pool = BondedPool::::get(1).unwrap(); + assert_eq!(bonded_pool.roles.root.unwrap(), 900); + assert_eq!(bonded_pool.roles.nominator.unwrap(), 901); + assert_eq!(bonded_pool.roles.bouncer.unwrap(), 902); + + // When the nominator tries to kick, then its a noop + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(901), 100, 1), + Error::::NotKickerOrDestroying + ); + + // When the root kicks then its ok + // Account with ID 100 is kicked. + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(900), 100, 1)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Bonded { member: 200, pool_id: 1, bonded: 200, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, + ] + ); + + // When the bouncer kicks then its ok + // Account with ID 200 is kicked. + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(902), 200, 1)); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { + member: 200, + pool_id: 1, + points: 200, + balance: 200, + era: 3 + }] + ); + + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Blocked, + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 10); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 100 + 200, balance: 100 + 200 } + }, + } + ); + assert_eq!( + *UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), + vec![(3, 100), (3, 200)], + ); + }); +} + +#[test] +fn unbond_permissionless_works() { + // Scenarios where non-admin accounts can unbond others + ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { + // Given the pool is blocked + unsafe_set_state(1, PoolState::Blocked); + + // A permissionless unbond attempt errors + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(420), 100, 1), + Error::::NotKickerOrDestroying + ); + + // permissionless unbond must be full + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(420), 100, 1, 80), + Error::::PartialUnbondNotAllowedPermissionlessly, + ); + + // Given the pool is destroying + unsafe_set_state(1, PoolState::Destroying); + + // The depositor cannot be fully unbonded until they are the last member + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(10), 10, 1), + Error::::MinimumBondNotMet, + ); + + // Any account can unbond a member that is not the depositor + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(420), 100, 1)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 } + ] + ); + + // still permissionless unbond must be full + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(420), 100, 1, 80), + Error::::PartialUnbondNotAllowedPermissionlessly, + ); + + // Given the pool is blocked + unsafe_set_state(1, PoolState::Blocked); + + // The depositor cannot be unbonded + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(420), 10, 1), + Error::::DoesNotHavePermission + ); + + // Given the pools is destroying + unsafe_set_state(1, PoolState::Destroying); + + // The depositor cannot be unbonded yet. + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(420), 10, 1), + Error::::DoesNotHavePermission, + ); + + // but when everyone is unbonded it can.. + CurrentEra::set(3); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 100, 1, 0)); + + // still permissionless unbond must be full. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(420), 10, 1, 5), + Error::::PartialUnbondNotAllowedPermissionlessly, + ); + + // depositor can never be unbonded permissionlessly . + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(420), 10, 1), + Error::::DoesNotHavePermission + ); + // but depositor itself can do it. + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(10), 10, 1)); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 + 3 => UnbondPool { points: 10, balance: 10 } + } + } + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); + assert_eq!( + *UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), + vec![(6, 10)] + ); + }); +} + +#[test] +fn partial_unbond_era_tracking() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // to make the depositor capable of withdrawing. + StakingMinBond::set(1); + MinCreateBond::::set(1); + MinJoinBond::::set(1); + assert_eq!(Lst::depositor_min_bond(), 1); + + // given + assert!(SubPoolsStorage::::get(1).is_none()); + assert_eq!(CurrentEra::get(), 0); + assert_eq!(BondingDuration::get(), 3); + + // so the depositor can leave, just keeps the test simpler. + unsafe_set_state(1, PoolState::Destroying); + + // when: casual unbond + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 1)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 3 } + ] + ); + + // when: casual further unbond, same era. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 5)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 3 }] + ); + + // when: casual further unbond, next era. + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 5)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 }] + ); + + // when: unbonding more than our active: error + assert_noop!( + frame_support::storage::with_storage_layer(|| Lst::unbond( + RuntimeOrigin::signed(10), + 10, + 1, + 5 + )), + Error::::MinimumBondNotMet + ); + // instead: + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 3)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 4, balance: 4 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 4 }] + ); + }); +} + +#[test] +fn partial_unbond_max_chunks() { + ExtBuilder::default().add_members(vec![(20, 20)]).ed(1).build_and_execute(|| { + MaxUnbonding::set(2); + + // given + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 2)); + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 3)); + + // when + CurrentEra::set(2); + assert_noop!( + frame_support::storage::with_storage_layer(|| Lst::unbond( + RuntimeOrigin::signed(20), + 20, + 1, + 4 + )), + Error::::MaxUnbondingLimit + ); + + // when + MaxUnbonding::set(3); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 1, 3)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Unbonded { member: 20, pool_id: 1, points: 2, balance: 2, era: 3 }, + Event::Unbonded { member: 20, pool_id: 1, points: 3, balance: 3, era: 4 }, + Event::Unbonded { member: 20, pool_id: 1, points: 1, balance: 1, era: 5 } + ] + ); + }) +} + +// depositor can unbond only up to `MinCreateBond`. +#[test] +fn depositor_permissioned_partial_unbond() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // given + StakingMinBond::set(5); + assert_eq!(Lst::depositor_min_bond(), 5); + + // can unbond a bit.. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 3)); + + // but not less than 2 + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 6), + Error::::MinimumBondNotMet + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 3 } + ] + ); + }); +} + +#[test] +fn depositor_permissioned_partial_unbond_slashed() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // given + assert_eq!(MinCreateBond::::get(), 2); + + // slash the default pool + StakingMock::slash_by(1, 5); + + // cannot unbond even 7, because the value of shares is now less. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 1, 7), + Error::::MinimumBondNotMet + ); + }); +} diff --git a/pallets/tangle-lst/src/tests/update_roles.rs b/pallets/tangle-lst/src/tests/update_roles.rs new file mode 100644 index 000000000..c3287f9e6 --- /dev/null +++ b/pallets/tangle-lst/src/tests/update_roles.rs @@ -0,0 +1,182 @@ +use super::*; +use frame_support::assert_err; +use frame_support::assert_noop; +use frame_support::assert_ok; + +#[test] +fn update_roles_works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), bouncer: Some(902) }, + ); + + // non-existent pools + assert_noop!( + Lst::update_roles( + RuntimeOrigin::signed(1), + 2, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::PoolNotFound, + ); + + // depositor cannot change roles. + assert_noop!( + Lst::update_roles( + RuntimeOrigin::signed(1), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::DoesNotHavePermission, + ); + + // nominator cannot change roles. + assert_noop!( + Lst::update_roles( + RuntimeOrigin::signed(901), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::DoesNotHavePermission, + ); + // bouncer + assert_noop!( + Lst::update_roles( + RuntimeOrigin::signed(902), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), + Error::::DoesNotHavePermission, + ); + + // but root can + assert_ok!(Lst::update_roles( + RuntimeOrigin::signed(900), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + )); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::RolesUpdated { root: Some(5), bouncer: Some(7), nominator: Some(6) } + ] + ); + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(5), nominator: Some(6), bouncer: Some(7) }, + ); + + // also root origin can + assert_ok!(Lst::update_roles( + RuntimeOrigin::root(), + 1, + ConfigOp::Set(1), + ConfigOp::Set(2), + ConfigOp::Set(3) + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { root: Some(1), bouncer: Some(3), nominator: Some(2) }] + ); + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(1), nominator: Some(2), bouncer: Some(3) }, + ); + + // Noop works + assert_ok!(Lst::update_roles( + RuntimeOrigin::root(), + 1, + ConfigOp::Set(11), + ConfigOp::Noop, + ConfigOp::Noop + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { root: Some(11), bouncer: Some(3), nominator: Some(2) }] + ); + + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(11), nominator: Some(2), bouncer: Some(3) }, + ); + + // Remove works + assert_ok!(Lst::update_roles( + RuntimeOrigin::root(), + 1, + ConfigOp::Set(69), + ConfigOp::Remove, + ConfigOp::Remove + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { root: Some(69), bouncer: None, nominator: None }] + ); + + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { depositor: 10, root: Some(69), nominator: None, bouncer: None }, + ); + }) +} + +const DOT: Balance = 10u128.pow(10u32); +const POLKADOT_TOTAL_ISSUANCE_GENESIS: Balance = DOT * 10u128.pow(9u32); + +const fn inflation(years: u128) -> u128 { + let mut i = 0; + let mut start = POLKADOT_TOTAL_ISSUANCE_GENESIS; + while i < years { + start = start + start / 10; + i += 1 + } + start +} + +#[test] +fn reward_counter_update_can_fail_if_pool_is_highly_slashed() { + // create a pool that has roughly half of the polkadot issuance in 10 years. + let pool_bond = inflation(10) / 2; + ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { + member: 10, + pool_id: 1, + bonded: 12_968_712_300_500_000_000, + joined: true, + } + ] + ); + + // slash this pool by 99% of that. + StakingMock::slash_by(1, pool_bond * 99 / 100); + + // some whale now joins with the other half ot the total issuance. This will trigger an + // overflow. This test is actually a bit too lenient because all the reward counters are + // set to zero. In other tests that we want to assert a scenario won't fail, we should + // also set the reward counters to some large value. + Currency::make_free_balance_be(&20, pool_bond * 2); + assert_err!(Lst::join(RuntimeOrigin::signed(20), pool_bond, 1), Error::::OverflowRisk); + }) +} diff --git a/pallets/tangle-lst/src/tests/withdraw_unbonded.rs b/pallets/tangle-lst/src/tests/withdraw_unbonded.rs new file mode 100644 index 000000000..86001457a --- /dev/null +++ b/pallets/tangle-lst/src/tests/withdraw_unbonded.rs @@ -0,0 +1,908 @@ +use super::*; + +macro_rules! unbonding_pools_with_era { + ($($k:expr => $v:expr),* $(,)?) => {{ + use sp_std::iter::{Iterator, IntoIterator}; + let not_bounded: BTreeMap<_, _> = Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*])); + BoundedBTreeMap::, TotalUnbondingPools>::try_from(not_bounded).unwrap() + }}; +} + +macro_rules! member_unbonding_eras { + ($( $any:tt )*) => {{ + let x: BoundedBTreeMap = bounded_btree_map!($( $any )*); + x + }}; +} + +#[test] +fn pool_withdraw_unbonded_works() { + ExtBuilder::default().add_members(vec![(20, 10)]).build_and_execute(|| { + // Given 10 unbond'ed directly against the pool account + + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 5)); + + assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15)); + assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(20)); + assert_eq!(Balances::free_balance(&default_bonded_account()), 20); + + // When + CurrentEra::set(StakingMock::current_era() + StakingMock::bonding_duration() + 1); + assert_ok!(Lst::pool_withdraw_unbonded(RuntimeOrigin::signed(10), 1, 0)); + + // Then their unbonding balance is no longer locked + assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15)); + assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(15)); + assert_eq!(Balances::free_balance(&default_bonded_account()), 20); + }); +} + +use sp_runtime::bounded_btree_map; + +#[test] +fn withdraw_unbonded_works_against_slashed_no_era_sub_pool() { + ExtBuilder::default() + .add_members(vec![(40, 40), (550, 550)]) + .build_and_execute(|| { + // reduce the noise a bit. + let _ = balances_events_since_last_call(); + + // Given + assert_eq!(StakingMock::bonding_duration(), 3); + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(550), 550)); + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(40), 40)); + assert_eq!(Currency::free_balance(&default_bonded_account()), 600); + + let mut current_era = 1; + CurrentEra::set(current_era); + + let mut sub_pools = SubPoolsStorage::::get(1).unwrap(); + let unbond_pool = sub_pools.with_era.get_mut(&3).unwrap(); + // Sanity check + assert_eq!(*unbond_pool, UnbondPool { points: 550 + 40, balance: 550 + 40 }); + assert_eq!(TotalValueLocked::::get(), 600); + + // Simulate a slash to the pool with_era(current_era), decreasing the balance by + // half + { + unbond_pool.balance /= 2; // 295 + SubPoolsStorage::::insert(1, sub_pools); + + // Adjust the TVL for this non-api usage (direct sub-pool modification) + TotalValueLocked::::mutate(|x| *x -= 295); + + // Update the equivalent of the unbonding chunks for the `StakingMock` + let mut x = UnbondingBalanceMap::get(); + x.get_mut(&default_bonded_account()) + .unwrap() + .get_mut(current_era as usize) + .unwrap() + .1 /= 2; + UnbondingBalanceMap::set(&x); + + Currency::make_free_balance_be( + &default_bonded_account(), + Currency::free_balance(&default_bonded_account()) / 2, // 300 + ); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 10); + StakingMock::slash_by(1, 5); + assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 5); + }; + + // Advance the current_era to ensure all `with_era` pools will be merged into + // `no_era` pool + current_era += TotalUnbondingPools::::get(); + CurrentEra::set(current_era); + + // Simulate some other call to unbond that would merge `with_era` pools into + // `no_era` + let sub_pools = + SubPoolsStorage::::get(1).unwrap().maybe_merge_pools(current_era); + SubPoolsStorage::::insert(1, sub_pools); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: UnbondPool { points: 550 + 40, balance: 275 + 20 }, + with_era: Default::default() + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, + Event::Unbonded { member: 550, pool_id: 1, points: 550, balance: 550, era: 3 }, + Event::Unbonded { member: 40, pool_id: 1, points: 40, balance: 40, era: 3 }, + Event::PoolSlashed { pool_id: 1, balance: 5 } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Burned { who: default_bonded_account(), amount: 300 }] + ); + + // When + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(550), 550, 0)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().no_era, + UnbondPool { points: 40, balance: 20 } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 550, pool_id: 1, balance: 275, points: 550 }, + Event::MemberRemoved { pool_id: 1, member: 550 } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 550, amount: 275 }] + ); + + // When + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(40), 40, 0)); + + // Then + assert_eq!( + SubPoolsStorage::::get(1).unwrap().no_era, + UnbondPool { points: 0, balance: 0 } + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 40, pool_id: 1, balance: 20, points: 40 }, + Event::MemberRemoved { pool_id: 1, member: 40 } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 40, amount: 20 }] + ); + + // now, finally, the depositor can take out its share. + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(10, 1)); + + current_era += 3; + CurrentEra::set(current_era); + + // when + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Unbonded { member: 10, pool_id: 1, balance: 5, points: 5, era: 9 }, + Event::Withdrawn { member: 10, pool_id: 1, balance: 5, points: 5 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 } + ] + ); + assert!(!Metadata::::contains_key(1)); + assert_eq!( + balances_events_since_last_call(), + vec![ + BEvent::Transfer { from: default_bonded_account(), to: 10, amount: 5 }, + BEvent::Thawed { who: default_reward_account(), amount: 5 }, + BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } + ] + ); + }); +} + +#[test] +fn withdraw_unbonded_works_against_slashed_with_era_sub_pools() { + ExtBuilder::default() + .add_members(vec![(40, 40), (550, 550)]) + .build_and_execute(|| { + let _ = balances_events_since_last_call(); + + // Given + // current bond is 600, we slash it all to 300. + StakingMock::slash_by(1, 300); + Currency::make_free_balance_be(&default_bonded_account(), 300); + assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(300)); + + assert_ok!(fully_unbond_permissioned(40, 1)); + assert_ok!(fully_unbond_permissioned(550, 1)); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 550 / 2 + 40 / 2, balance: 550 / 2 + 40 / 2 + }} + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, + Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, + Event::PoolSlashed { pool_id: 1, balance: 300 }, + Event::Unbonded { member: 40, pool_id: 1, balance: 20, points: 20, era: 3 }, + Event::Unbonded { member: 550, pool_id: 1, balance: 275, points: 275, era: 3 } + ] + ); + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Burned { who: default_bonded_account(), amount: 300 },] + ); + + CurrentEra::set(StakingMock::bonding_duration()); + + // When + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(40), 40, 0)); + + // Then + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 40, amount: 20 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 40, pool_id: 1, balance: 20, points: 20 }, + Event::MemberRemoved { pool_id: 1, member: 40 } + ] + ); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 3 => UnbondPool { points: 550 / 2, balance: 550 / 2 }} + ); + + // When + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(550), 550, 0)); + + // Then + assert_eq!( + balances_events_since_last_call(), + vec![BEvent::Transfer { from: default_bonded_account(), to: 550, amount: 275 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 550, pool_id: 1, balance: 275, points: 275 }, + Event::MemberRemoved { pool_id: 1, member: 550 } + ] + ); + assert!(SubPoolsStorage::::get(1).unwrap().with_era.is_empty()); + + // now, finally, the depositor can take out its share. + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(fully_unbond_permissioned(10, 1)); + + // because everyone else has left, the points + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + unbonding_pools_with_era! { 6 => UnbondPool { points: 5, balance: 5 }} + ); + + CurrentEra::set(CurrentEra::get() + 3); + + // set metadata to check that it's being removed on dissolve + assert_ok!(Lst::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); + assert!(Metadata::::contains_key(1)); + + // when + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // then + assert_eq!(Currency::free_balance(&10), 10 + 35); + assert_eq!(Currency::free_balance(&default_bonded_account()), 0); + + // in this test 10 also gets a fair share of the slash, because the slash was + // applied to the bonded account. + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 6 }, + Event::Withdrawn { member: 10, pool_id: 1, points: 5, balance: 5 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 } + ] + ); + assert!(!Metadata::::contains_key(1)); + assert_eq!( + balances_events_since_last_call(), + vec![ + BEvent::Transfer { from: default_bonded_account(), to: 10, amount: 5 }, + BEvent::Thawed { who: default_reward_account(), amount: 5 }, + BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } + ] + ); + }); +} + +#[test] +fn withdraw_unbonded_handles_faulty_sub_pool_accounting() { + ExtBuilder::default().build_and_execute(|| { + // Given + assert_eq!(>::minimum_balance(), 5); + assert_eq!(Currency::free_balance(&10), 35); + assert_eq!(Currency::free_balance(&default_bonded_account()), 10); + unsafe_set_state(1, PoolState::Destroying); + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(10), 10)); + + // Simulate a slash that is not accounted for in the sub pools. + Currency::make_free_balance_be(&default_bonded_account(), 5); + assert_eq!( + SubPoolsStorage::::get(1).unwrap().with_era, + //------------------------------balance decrease is not account for + unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 } } + ); + + CurrentEra::set(3); + + // When + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // Then + assert_eq!(Currency::free_balance(&10), 10 + 35); + assert_eq!(Currency::free_balance(&default_bonded_account()), 0); + }); +} + +#[test] +fn withdraw_unbonded_kick() { + ExtBuilder::default() + .add_members(vec![(100, 100), (200, 200)]) + .build_and_execute(|| { + // Given + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(100), 100)); + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(200), 200)); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + } + } + ); + CurrentEra::set(StakingMock::bonding_duration()); + + // Cannot kick when pool is open + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(902), 100, 0), + Error::::NotKickerOrDestroying + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Bonded { member: 200, pool_id: 1, bonded: 200, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, + Event::Unbonded { member: 200, pool_id: 1, points: 200, balance: 200, era: 3 } + ] + ); + + // Given + unsafe_set_state(1, PoolState::Blocked); + + // Cannot kick as a nominator + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(901), 100, 0), + Error::::NotKickerOrDestroying + ); + + // Can kick as root + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(900), 100, 0)); + + // Can kick as bouncer + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(900), 200, 0)); + + assert_eq!(Currency::free_balance(&100), 100 + 100); + assert_eq!(Currency::free_balance(&200), 200 + 200); + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 100, pool_id: 1, points: 100, balance: 100 }, + Event::MemberRemoved { pool_id: 1, member: 100 }, + Event::Withdrawn { member: 200, pool_id: 1, points: 200, balance: 200 }, + Event::MemberRemoved { pool_id: 1, member: 200 } + ] + ); + }); +} + +#[test] +fn withdraw_unbonded_destroying_permissionless() { + ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { + // Given + assert_ok!(Lst::fully_unbond(RuntimeOrigin::signed(100), 100)); + assert_eq!( + BondedPool::::get(1).unwrap(), + BondedPool { + id: 1, + inner: BondedPoolInner { + commission: Commission::default(), + roles: DEFAULT_ROLES, + state: PoolState::Open, + } + } + ); + CurrentEra::set(StakingMock::bonding_duration()); + assert_eq!(Currency::free_balance(&100), 100); + + // Cannot permissionlessly withdraw + assert_noop!( + Lst::fully_unbond(RuntimeOrigin::signed(420), 100), + Error::::NotKickerOrDestroying + ); + + // Given + unsafe_set_state(1, PoolState::Destroying); + + // Can permissionlessly withdraw a member that is not the depositor + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(420), 100, 0)); + + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default(),); + assert_eq!(Currency::free_balance(&100), 100 + 100); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, + Event::Withdrawn { member: 100, pool_id: 1, points: 100, balance: 100 }, + Event::MemberRemoved { pool_id: 1, member: 100 } + ] + ); + }); +} + +#[test] +fn partial_withdraw_unbonded_depositor() { + ExtBuilder::default().ed(1).build_and_execute(|| { + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + unsafe_set_state(1, PoolState::Destroying); + + // given + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 6)); + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 1)); + + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, + Event::Unbonded { member: 10, pool_id: 1, points: 6, balance: 6, era: 3 }, + Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 } + ] + ); + + // when + CurrentEra::set(2); + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0), + Error::::CannotWithdrawAny + ); + + // when + CurrentEra::set(3); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 10, pool_id: 1, points: 6, balance: 6 }] + ); + + // when + CurrentEra::set(4); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + // then + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 10, pool_id: 1, points: 1, balance: 1 },] + ); + + // when repeating: + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0), + Error::::CannotWithdrawAny + ); + }); +} + +#[test] +fn partial_withdraw_unbonded_non_depositor() { + ExtBuilder::default().add_members(vec![(11, 10)]).build_and_execute(|| { + // given + assert_ok!(Lst::unbond(RuntimeOrigin::signed(11), 11, 6)); + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(11), 11, 1)); + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 6, balance: 6 }, + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 11, pool_id: 1, bonded: 10, joined: true }, + Event::Unbonded { member: 11, pool_id: 1, points: 6, balance: 6, era: 3 }, + Event::Unbonded { member: 11, pool_id: 1, points: 1, balance: 1, era: 4 } + ] + ); + + // when + CurrentEra::set(2); + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), + Error::::CannotWithdrawAny + ); + + // when + CurrentEra::set(3); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 4 => UnbondPool { points: 1, balance: 1 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 11, pool_id: 1, points: 6, balance: 6 }] + ); + + // when + CurrentEra::set(4); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0)); + + // then + assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 11, pool_id: 1, points: 1, balance: 1 }] + ); + + // when repeating: + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), + Error::::CannotWithdrawAny + ); + }); +} + +#[test] +fn full_multi_step_withdrawing_non_depositor() { + ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { + assert_eq!(TotalValueLocked::::get(), 110); + // given + assert_ok!(Lst::unbond(RuntimeOrigin::signed(100), 100, 75)); + + // tvl unchanged. + assert_eq!(TotalValueLocked::::get(), 110); + + // progress one era and unbond the leftover. + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(100), 100, 25)); + + assert_noop!( + Lst::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0), + Error::::CannotWithdrawAny + ); + // tvl unchanged. + assert_eq!(TotalValueLocked::::get(), 110); + + // now the 75 should be free. + CurrentEra::set(3); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, + Event::Unbonded { member: 100, pool_id: 1, points: 75, balance: 75, era: 3 }, + Event::Unbonded { member: 100, pool_id: 1, points: 25, balance: 25, era: 4 }, + Event::Withdrawn { member: 100, pool_id: 1, points: 75, balance: 75 }, + ] + ); + // tvl updated + assert_eq!(TotalValueLocked::::get(), 35); + + // the 25 should be free now, and the member removed. + CurrentEra::set(4); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0)); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 100, pool_id: 1, points: 25, balance: 25 }, + Event::MemberRemoved { pool_id: 1, member: 100 } + ] + ); + }) +} + +#[test] +fn out_of_sync_unbonding_chunks() { + // the unbonding_eras in pool member are always fixed to the era at which they are unlocked, + // but the actual unbonding pools get pruned and might get combined in the no_era pool. + // Lst are only merged when one unbonds, so we unbond a little bit on every era to + // simulate this. + ExtBuilder::default() + .add_members(vec![(20, 100), (30, 100)]) + .build_and_execute(|| { + System::reset_events(); + + // when + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 5)); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(30), 30, 5)); + + // then member-local unbonding is pretty much in sync with the global pools. + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 10, balance: 10 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 3 }, + Event::Unbonded { member: 30, pool_id: 1, points: 5, balance: 5, era: 3 }, + ] + ); + + // when + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 5)); + + // then still member-local unbonding is pretty much in sync with the global pools. + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 10, balance: 10 }, + 4 => UnbondPool { points: 5, balance: 5 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 4 }] + ); + + // when + CurrentEra::set(2); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 5)); + + // then still member-local unbonding is pretty much in sync with the global pools. + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 3 => UnbondPool { points: 10, balance: 10 }, + 4 => UnbondPool { points: 5, balance: 5 }, + 5 => UnbondPool { points: 5, balance: 5 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 }] + ); + + // when + CurrentEra::set(5); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 5)); + + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + // era 3 is merged into no_era. + no_era: UnbondPool { points: 10, balance: 10 }, + with_era: unbonding_pools_with_era! { + 4 => UnbondPool { points: 5, balance: 5 }, + 5 => UnbondPool { points: 5, balance: 5 }, + 8 => UnbondPool { points: 5, balance: 5 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 8 }] + ); + + // now we start withdrawing unlocked bonds. + + // when + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + // era 3 is merged into no_era. + no_era: UnbondPool { points: 5, balance: 5 }, + with_era: unbonding_pools_with_era! { + 8 => UnbondPool { points: 5, balance: 5 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 20, pool_id: 1, points: 15, balance: 15 }] + ); + + // when + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(30), 30, 0)); + // then + assert_eq!( + SubPoolsStorage::::get(1).unwrap(), + SubPools { + // era 3 is merged into no_era. + no_era: Default::default(), + with_era: unbonding_pools_with_era! { + 8 => UnbondPool { points: 5, balance: 5 } + } + } + ); + assert_eq!( + pool_events_since_last_call(), + vec![Event::Withdrawn { member: 30, pool_id: 1, points: 5, balance: 5 }] + ); + }) +} + +#[test] +fn full_multi_step_withdrawing_depositor() { + ExtBuilder::default().ed(1).build_and_execute(|| { + // depositor now has 20, they can unbond to 10. + assert_eq!(Lst::depositor_min_bond(), 10); + assert_ok!(Lst::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); + + // now they can. + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 7)); + + // progress one era and unbond the leftover. + CurrentEra::set(1); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 3)); + + // they can't unbond to a value below 10 other than 0.. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 5), + Error::::MinimumBondNotMet + ); + + // but not even full, because they pool is not yet destroying. + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 10), + Error::::MinimumBondNotMet + ); + + // but now they can. + unsafe_set_state(1, PoolState::Destroying); + assert_noop!( + Lst::unbond(RuntimeOrigin::signed(10), 10, 5), + Error::::MinimumBondNotMet + ); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(10), 10, 10)); + + // now the 7 should be free. + CurrentEra::set(3); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, + Event::Unbonded { member: 10, pool_id: 1, balance: 7, points: 7, era: 3 }, + Event::Unbonded { member: 10, pool_id: 1, balance: 3, points: 3, era: 4 }, + Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 }, + Event::Withdrawn { member: 10, pool_id: 1, balance: 7, points: 7 } + ] + ); + + // the 13 should be free now, and the member removed. + CurrentEra::set(4); + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 10, pool_id: 1, points: 13, balance: 13 }, + Event::MemberRemoved { pool_id: 1, member: 10 }, + Event::Destroyed { pool_id: 1 }, + ] + ); + assert!(!Metadata::::contains_key(1)); + }) +} + +#[test] +fn withdraw_unbonded_removes_claim_permissions_on_leave() { + ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { + // Given + CurrentEra::set(1); + + assert_ok!(Lst::set_claim_permission( + RuntimeOrigin::signed(20), + ClaimPermission::PermissionlessAll + )); + assert_ok!(Lst::unbond(RuntimeOrigin::signed(20), 20, 20)); + assert_eq!(ClaimPermissions::::get(20), ClaimPermission::PermissionlessAll); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Created { depositor: 10, pool_id: 1 }, + Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, + Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 4 }, + ] + ); + + CurrentEra::set(5); + + // When + assert_ok!(Lst::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + Event::Withdrawn { member: 20, pool_id: 1, balance: 20, points: 20 }, + Event::MemberRemoved { pool_id: 1, member: 20 } + ] + ); + }); +} diff --git a/pallets/tangle-lst/src/types/bonded_pool.rs b/pallets/tangle-lst/src/types/bonded_pool.rs new file mode 100644 index 000000000..0c5149585 --- /dev/null +++ b/pallets/tangle-lst/src/types/bonded_pool.rs @@ -0,0 +1,382 @@ +use super::*; + +/// Pool permissions and state +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Clone)] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct BondedPoolInner { + /// The commission rate of the pool. + pub commission: Commission, + /// See [`PoolRoles`]. + pub roles: PoolRoles, + /// The current state of the pool. + pub state: PoolState, +} + +/// A wrapper for bonded pools, with utility functions. +/// +/// The main purpose of this is to wrap a [`BondedPoolInner`], with the account +/// + id of the pool, for easier access. +#[derive(RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq))] +pub struct BondedPool { + /// The identifier of the pool. + pub id: PoolId, + /// The inner fields. + pub inner: BondedPoolInner, +} + +impl sp_std::ops::Deref for BondedPool { + type Target = BondedPoolInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl sp_std::ops::DerefMut for BondedPool { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl BondedPool { + /// Create a new bonded pool with the given roles and identifier. + pub fn new(id: PoolId, roles: PoolRoles) -> Self { + Self { + id, + inner: BondedPoolInner { + commission: Commission::default(), + roles, + state: PoolState::Open, + }, + } + } + + /// Get [`Self`] from storage. Returns `None` if no entry for `pool_account` exists. + pub fn get(id: PoolId) -> Option { + BondedPools::::try_get(id).ok().map(|inner| Self { id, inner }) + } + + /// Get the bonded account id of this pool. + pub fn bonded_account(&self) -> T::AccountId { + Pallet::::create_bonded_account(self.id) + } + + /// Get the reward account id of this pool. + pub fn reward_account(&self) -> T::AccountId { + Pallet::::create_reward_account(self.id) + } + + /// Consume self and put into storage. + pub fn put(self) { + BondedPools::::insert(self.id, self.inner); + } + + pub fn points(&self) -> BalanceOf { + // the total points of the pool is the total supply of LST token of the pool + T::Fungibles::total_issuance(self.id.into()) + } + + /// Consume self and remove from storage. + pub fn remove(self) { + BondedPools::::remove(self.id); + } + + /// Convert the given amount of balance to points given the current pool state. + /// + /// This is often used for bonding and issuing new funds into the pool. + pub fn balance_to_point(&self, new_funds: BalanceOf) -> BalanceOf { + let bonded_balance = + T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero()); + Pallet::::balance_to_point(bonded_balance, self.points(), new_funds) + } + + /// Convert the given number of points to balance given the current pool state. + /// + /// This is often used for unbonding. + pub fn points_to_balance(&self, points: BalanceOf) -> BalanceOf { + let bonded_balance = + T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero()); + Pallet::::point_to_balance(bonded_balance, self.points(), points) + } + + /// Issue points to [`Self`] for `new_funds`. + pub fn issue(&mut self, new_funds: BalanceOf) -> BalanceOf { + self.balance_to_point(new_funds) + } + + /// Dissolve some points from the pool i.e. unbond the given amount of points from this pool. + /// This is the opposite of issuing some funds into the pool. + /// + /// Mutates self in place, but does not write anything to storage. + /// + /// Returns the equivalent balance amount that actually needs to get unbonded. + pub fn dissolve(&mut self, points: BalanceOf) -> BalanceOf { + // NOTE: do not optimize by removing `balance`. it must be computed before mutating + // `self.point`. + self.points_to_balance(points) + } + + /// The pools balance that is transferable provided it is expendable by staking pallet. + pub fn transferable_balance(&self) -> BalanceOf { + let account = self.bonded_account(); + // Note on why we can't use `Currency::reducible_balance`: Since pooled account has a + // provider (staking pallet), the account can not be set expendable by + // `pallet-nomination-pool`. This means reducible balance always returns balance preserving + // ED in the account. What we want though is transferable balance given the account can be + // dusted. + T::Currency::free_balance(&account) + .saturating_sub(T::Staking::active_stake(&account).unwrap_or_default()) + } + + pub fn is_root(&self, who: &T::AccountId) -> bool { + self.roles.root.as_ref().map_or(false, |root| root == who) + } + + pub fn is_bouncer(&self, who: &T::AccountId) -> bool { + self.roles.bouncer.as_ref().map_or(false, |bouncer| bouncer == who) + } + + pub fn can_update_roles(&self, who: &T::AccountId) -> bool { + self.is_root(who) + } + + pub fn can_nominate(&self, who: &T::AccountId) -> bool { + self.is_root(who) + || self.roles.nominator.as_ref().map_or(false, |nominator| nominator == who) + } + + pub fn can_kick(&self, who: &T::AccountId) -> bool { + self.state == PoolState::Blocked && (self.is_root(who) || self.is_bouncer(who)) + } + + pub fn can_toggle_state(&self, who: &T::AccountId) -> bool { + (self.is_root(who) || self.is_bouncer(who)) && !self.is_destroying() + } + + pub fn can_set_metadata(&self, who: &T::AccountId) -> bool { + self.is_root(who) || self.is_bouncer(who) + } + + pub fn can_manage_commission(&self, who: &T::AccountId) -> bool { + self.is_root(who) + } + + pub fn can_claim_commission(&self, who: &T::AccountId) -> bool { + if let Some(permission) = self.commission.claim_permission.as_ref() { + match permission { + CommissionClaimPermission::Permissionless => true, + CommissionClaimPermission::Account(account) => account == who || self.is_root(who), + } + } else { + self.is_root(who) + } + } + + pub fn is_destroying(&self) -> bool { + matches!(self.state, PoolState::Destroying) + } + + pub fn is_destroying_and_only_depositor(&self, alleged_depositor_points: BalanceOf) -> bool { + // initial `MinCreateBond` (or more) is what guarantees that the ledger of the pool does not + // get killed in the staking system, and that it does not fall below `MinimumNominatorBond`, + // which could prevent other non-depositor members from fully leaving. Thus, all members + // must withdraw, then depositor can unbond, and finally withdraw after waiting another + // cycle. + self.is_destroying() && self.points() == alleged_depositor_points + } + + /// Whether or not the pool is ok to be in `PoolSate::Open`. If this returns an `Err`, then the + /// pool is unrecoverable and should be in the destroying state. + pub fn ok_to_be_open(&self) -> Result<(), DispatchError> { + ensure!(!self.is_destroying(), Error::::CanNotChangeState); + + let bonded_balance = + T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero()); + ensure!(!bonded_balance.is_zero(), Error::::OverflowRisk); + + let points_to_balance_ratio_floor = self + .points() + // We checked for zero above + .div(bonded_balance); + + let max_points_to_balance = T::MaxPointsToBalance::get(); + + // Pool points can inflate relative to balance, but only if the pool is slashed. + // If we cap the ratio of points:balance so one cannot join a pool that has been slashed + // by `max_points_to_balance`%, if not zero. + ensure!( + points_to_balance_ratio_floor < max_points_to_balance.into(), + Error::::OverflowRisk + ); + + // then we can be decently confident the bonding pool points will not overflow + // `BalanceOf`. Note that these are just heuristics. + + Ok(()) + } + + /// Check that the pool can accept a member with `new_funds`. + pub fn ok_to_join(&self) -> Result<(), DispatchError> { + ensure!(self.state == PoolState::Open, Error::::NotOpen); + self.ok_to_be_open()?; + Ok(()) + } + + pub fn ok_to_unbond_with( + &self, + caller: &T::AccountId, + target_account: &T::AccountId, + active_points: BalanceOf, + unbonding_points: BalanceOf, + ) -> Result<(), DispatchError> { + let is_permissioned = caller == target_account; + let is_depositor = *target_account == self.roles.depositor; + let is_full_unbond = unbonding_points == active_points; + + let balance_after_unbond = active_points.saturating_sub(unbonding_points); + + // any partial unbonding is only ever allowed if this unbond is permissioned. + ensure!( + is_permissioned || is_full_unbond, + Error::::PartialUnbondNotAllowedPermissionlessly + ); + + // any unbond must comply with the balance condition: + ensure!( + is_full_unbond + || balance_after_unbond + >= if is_depositor { + Pallet::::depositor_min_bond() + } else { + MinJoinBond::::get() + }, + Error::::MinimumBondNotMet + ); + + // additional checks: + match (is_permissioned, is_depositor) { + (true, false) => (), + (true, true) => { + // permission depositor unbond: if destroying and pool is empty, always allowed, + // with no additional limits. + if self.is_destroying_and_only_depositor(balance_after_unbond) { + // everything good, let them unbond anything. + } else { + // depositor cannot fully unbond yet. + ensure!(!is_full_unbond, Error::::MinimumBondNotMet); + } + }, + (false, false) => { + // If the pool is blocked, then an admin with kicking permissions can remove a + // member. If the pool is being destroyed, anyone can remove a member + debug_assert!(is_full_unbond); + ensure!( + self.can_kick(caller) || self.is_destroying(), + Error::::NotKickerOrDestroying + ) + }, + (false, true) => { + // the depositor can simply not be unbonded permissionlessly, period. + return Err(Error::::DoesNotHavePermission.into()); + }, + }; + + Ok(()) + } + + /// # Returns + /// + /// * Ok(()) if [`Call::withdraw_unbonded`] can be called, `Err(DispatchError)` otherwise. + pub fn ok_to_withdraw_unbonded_with( + &self, + caller: &T::AccountId, + target_account: &T::AccountId, + ) -> Result<(), DispatchError> { + // This isn't a depositor + let is_permissioned = caller == target_account; + ensure!( + is_permissioned || self.can_kick(caller) || self.is_destroying(), + Error::::NotKickerOrDestroying + ); + Ok(()) + } + + /// Bond exactly `amount` from `who`'s funds into this pool. Increases the [`TotalValueLocked`] + /// by `amount`. + /// + /// If the bond is [`BondType::Create`], [`Staking::bond`] is called, and `who` is allowed to be + /// killed. Otherwise, [`Staking::bond_extra`] is called and `who` cannot be killed. + /// + /// Returns `Ok(points_issues)`, `Err` otherwise. + pub fn try_bond_funds( + &mut self, + who: &T::AccountId, + amount: BalanceOf, + ty: BondType, + ) -> Result, DispatchError> { + // Cache the value + let bonded_account = self.bonded_account(); + T::Currency::transfer( + who, + &bonded_account, + amount, + match ty { + BondType::Create => ExistenceRequirement::KeepAlive, + BondType::Later => ExistenceRequirement::AllowDeath, + }, + )?; + // We must calculate the points issued *before* we bond who's funds, else points:balance + // ratio will be wrong. + let points_issued = self.issue(amount); + + match ty { + BondType::Create => T::Staking::bond(&bonded_account, amount, &self.reward_account())?, + // The pool should always be created in such a way its in a state to bond extra, but if + // the active balance is slashed below the minimum bonded or the account cannot be + // found, we exit early. + BondType::Later => T::Staking::bond_extra(&bonded_account, amount)?, + } + TotalValueLocked::::mutate(|tvl| { + tvl.saturating_accrue(amount); + }); + + // finally mint the pool token + T::Fungibles::mint_into(self.id.into(), who, points_issued)?; + + Ok(points_issued) + } + + // Set the state of `self`, and deposit an event if the state changed. State should never be set + // directly in in order to ensure a state change event is always correctly deposited. + pub fn set_state(&mut self, state: PoolState) { + if self.state != state { + self.state = state; + Pallet::::deposit_event(Event::::StateChanged { + pool_id: self.id, + new_state: state, + }); + }; + } + + /// Withdraw all the funds that are already unlocked from staking for the + /// [`BondedPool::bonded_account`]. + /// + /// Also reduces the [`TotalValueLocked`] by the difference of the + /// [`T::Staking::total_stake`] of the [`BondedPool::bonded_account`] that might occur by + /// [`T::Staking::withdraw_unbonded`]. + /// + /// Returns the result of [`T::Staking::withdraw_unbonded`] + pub fn withdraw_from_staking(&self, num_slashing_spans: u32) -> Result { + let bonded_account = self.bonded_account(); + + let prev_total = T::Staking::total_stake(&bonded_account.clone()).unwrap_or_default(); + let outcome = T::Staking::withdraw_unbonded(bonded_account.clone(), num_slashing_spans); + let diff = prev_total + .defensive_saturating_sub(T::Staking::total_stake(&bonded_account).unwrap_or_default()); + TotalValueLocked::::mutate(|tvl| { + tvl.saturating_reduce(diff); + }); + outcome + } +} diff --git a/pallets/tangle-lst/src/types/commission.rs b/pallets/tangle-lst/src/types/commission.rs new file mode 100644 index 000000000..9fbee7550 --- /dev/null +++ b/pallets/tangle-lst/src/types/commission.rs @@ -0,0 +1,227 @@ +use super::*; + +// A pool's possible commission claiming permissions. +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum CommissionClaimPermission { + Permissionless, + Account(AccountId), +} + +/// Pool commission. +/// +/// The pool `root` can set commission configuration after pool creation. By default, all commission +/// values are `None`. Pool `root` can also set `max` and `change_rate` configurations before +/// setting an initial `current` commission. +/// +/// `current` is a tuple of the commission percentage and payee of commission. `throttle_from` +/// keeps track of which block `current` was last updated. A `max` commission value can only be +/// decreased after the initial value is set, to prevent commission from repeatedly increasing. +/// +/// An optional commission `change_rate` allows the pool to set strict limits to how much commission +/// can change in each update, and how often updates can take place. +#[derive( + Encode, Decode, DefaultNoBound, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Copy, Clone, +)] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct Commission { + /// Optional commission rate of the pool along with the account commission is paid to. + pub current: Option<(Perbill, T::AccountId)>, + /// Optional maximum commission that can be set by the pool `root`. Once set, this value can + /// only be updated to a decreased value. + pub max: Option, + /// Optional configuration around how often commission can be updated, and when the last + /// commission update took place. + pub change_rate: Option>>, + /// The block from where throttling should be checked from. This value will be updated on all + /// commission updates and when setting an initial `change_rate`. + pub throttle_from: Option>, + // Whether commission can be claimed permissionlessly, or whether an account can claim + // commission. `Root` role can always claim. + pub claim_permission: Option>, +} + +impl Commission { + /// Returns true if the current commission updating to `to` would exhaust the change rate + /// limits. + /// + /// A commission update will be throttled (disallowed) if: + /// 1. not enough blocks have passed since the `throttle_from` block, if exists, or + /// 2. the new commission is greater than the maximum allowed increase. + pub fn throttling(&self, to: &Perbill) -> bool { + if let Some(t) = self.change_rate.as_ref() { + let commission_as_percent = + self.current.as_ref().map(|(x, _)| *x).unwrap_or(Perbill::zero()); + + // do not throttle if `to` is the same or a decrease in commission. + if *to <= commission_as_percent { + return false; + } + // Test for `max_increase` throttling. + // + // Throttled if the attempted increase in commission is greater than `max_increase`. + if (*to).saturating_sub(commission_as_percent) > t.max_increase { + return true; + } + + // Test for `min_delay` throttling. + // + // Note: matching `None` is defensive only. `throttle_from` should always exist where + // `change_rate` has already been set, so this scenario should never happen. + return self.throttle_from.map_or_else( + || { + defensive!("throttle_from should exist if change_rate is set"); + true + }, + |f| { + // if `min_delay` is zero (no delay), not throttling. + if t.min_delay == Zero::zero() { + false + } else { + // throttling if blocks passed is less than `min_delay`. + let blocks_surpassed = + >::block_number().saturating_sub(f); + blocks_surpassed < t.min_delay + } + }, + ); + } + false + } + + /// Gets the pool's current commission, or returns Perbill::zero if none is set. + /// Bounded to global max if current is greater than `GlobalMaxCommission`. + pub fn current(&self) -> Perbill { + self.current + .as_ref() + .map_or(Perbill::zero(), |(c, _)| *c) + .min(GlobalMaxCommission::::get().unwrap_or(Bounded::max_value())) + } + + /// Set the pool's commission. + /// + /// Update commission based on `current`. If a `None` is supplied, allow the commission to be + /// removed without any change rate restrictions. Updates `throttle_from` to the current block. + /// If the supplied commission is zero, `None` will be inserted and `payee` will be ignored. + pub fn try_update_current( + &mut self, + current: &Option<(Perbill, T::AccountId)>, + ) -> DispatchResult { + self.current = match current { + None => None, + Some((commission, payee)) => { + ensure!(!self.throttling(commission), Error::::CommissionChangeThrottled); + ensure!( + commission <= &GlobalMaxCommission::::get().unwrap_or(Bounded::max_value()), + Error::::CommissionExceedsGlobalMaximum + ); + ensure!( + self.max.map_or(true, |m| commission <= &m), + Error::::CommissionExceedsMaximum + ); + if commission.is_zero() { + None + } else { + Some((*commission, payee.clone())) + } + }, + }; + self.register_update(); + Ok(()) + } + + /// Set the pool's maximum commission. + /// + /// The pool's maximum commission can initially be set to any value, and only smaller values + /// thereafter. If larger values are attempted, this function will return a dispatch error. + /// + /// If `current.0` is larger than the updated max commission value, `current.0` will also be + /// updated to the new maximum. This will also register a `throttle_from` update. + /// A `PoolCommissionUpdated` event is triggered if `current.0` is updated. + pub fn try_update_max(&mut self, pool_id: PoolId, new_max: Perbill) -> DispatchResult { + ensure!( + new_max <= GlobalMaxCommission::::get().unwrap_or(Bounded::max_value()), + Error::::CommissionExceedsGlobalMaximum + ); + if let Some(old) = self.max.as_mut() { + if new_max > *old { + return Err(Error::::MaxCommissionRestricted.into()); + } + *old = new_max; + } else { + self.max = Some(new_max) + }; + let updated_current = self + .current + .as_mut() + .map(|(c, _)| { + let u = *c > new_max; + *c = (*c).min(new_max); + u + }) + .unwrap_or(false); + + if updated_current { + if let Some((_, payee)) = self.current.as_ref() { + Pallet::::deposit_event(Event::::PoolCommissionUpdated { + pool_id, + current: Some((new_max, payee.clone())), + }); + } + self.register_update(); + } + Ok(()) + } + + /// Set the pool's commission `change_rate`. + /// + /// Once a change rate configuration has been set, only more restrictive values can be set + /// thereafter. These restrictions translate to increased `min_delay` values and decreased + /// `max_increase` values. + /// + /// Update `throttle_from` to the current block upon setting change rate for the first time, so + /// throttling can be checked from this block. + pub fn try_update_change_rate( + &mut self, + change_rate: CommissionChangeRate>, + ) -> DispatchResult { + ensure!(!&self.less_restrictive(&change_rate), Error::::CommissionChangeRateNotAllowed); + + if self.change_rate.is_none() { + self.register_update(); + } + self.change_rate = Some(change_rate); + Ok(()) + } + + /// Updates a commission's `throttle_from` field to the current block. + pub fn register_update(&mut self) { + self.throttle_from = Some(>::block_number()); + } + + /// Checks whether a change rate is less restrictive than the current change rate, if any. + /// + /// No change rate will always be less restrictive than some change rate, so where no + /// `change_rate` is currently set, `false` is returned. + pub fn less_restrictive(&self, new: &CommissionChangeRate>) -> bool { + self.change_rate + .as_ref() + .map(|c| new.max_increase > c.max_increase || new.min_delay < c.min_delay) + .unwrap_or(false) + } +} + +/// Pool commission change rate preferences. +/// +/// The pool root is able to set a commission change rate for their pool. A commission change rate +/// consists of 2 values; (1) the maximum allowed commission change, and (2) the minimum amount of +/// blocks that must elapse before commission updates are allowed again. +/// +/// Commission change rates are not applied to decreases in commission. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Copy, Clone)] +pub struct CommissionChangeRate { + /// The maximum amount the commission can be updated by per `min_delay` period. + pub max_increase: Perbill, + /// How often an update can take place. + pub min_delay: BlockNumber, +} diff --git a/pallets/tangle-lst/src/types/mod.rs b/pallets/tangle-lst/src/types/mod.rs new file mode 100644 index 000000000..f666215a3 --- /dev/null +++ b/pallets/tangle-lst/src/types/mod.rs @@ -0,0 +1,78 @@ +use super::*; +pub mod bonded_pool; +pub mod commission; +pub mod pools; +pub mod sub_pools; + +pub use bonded_pool::*; +pub use commission::*; +pub use pools::*; +pub use sub_pools::*; + +/// The balance type used by the currency system. +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Type used for unique identifier of each pool. +pub type PoolId = u32; + +pub type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +pub const POINTS_TO_BALANCE_INIT_RATIO: u32 = 1; +/// Possible operations on the configuration values of this pallet. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, PartialEq, Clone)] +pub enum ConfigOp { + /// Don't change. + Noop, + /// Set the given value. + Set(T), + /// Remove from storage. + Remove, +} + +/// The type of bonding that can happen to a pool. +pub enum BondType { + /// Someone is bonding into the pool upon creation. + Create, + /// Someone is adding more funds later to this pool. + Later, +} + +/// How to increase the bond of a member. +#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum BondExtra { + /// Take from the free balance. + FreeBalance(Balance), +} + +/// The type of account being created. +#[derive(Encode, Decode)] +pub enum AccountType { + Bonded, + Reward, +} + +/// The permission a pool member can set for other accounts to claim rewards on their behalf. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum ClaimPermission { + /// Only the pool member themself can claim their rewards. + Permissioned, + /// Anyone can compound rewards on a pool member's behalf. + PermissionlessCompound, + /// Anyone can withdraw rewards on a pool member's behalf. + PermissionlessWithdraw, + /// Anyone can withdraw and compound rewards on a pool member's behalf. + PermissionlessAll, +} + +impl ClaimPermission { + pub fn can_bond_extra(&self) -> bool { + matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessCompound) + } +} + +impl Default for ClaimPermission { + fn default() -> Self { + Self::Permissioned + } +} diff --git a/pallets/tangle-lst/src/types/pools.rs b/pallets/tangle-lst/src/types/pools.rs new file mode 100644 index 000000000..d14e1e2c1 --- /dev/null +++ b/pallets/tangle-lst/src/types/pools.rs @@ -0,0 +1,91 @@ +use super::*; + +/// A member in a pool. +#[derive( + Encode, + Decode, + MaxEncodedLen, + TypeInfo, + RuntimeDebugNoBound, + CloneNoBound, + frame_support::PartialEqNoBound, +)] +#[cfg_attr(feature = "std", derive(DefaultNoBound))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct PoolMember { + /// The identifier of the pool to which `who` belongs. + pub pool_id: PoolId, + /// The eras in which this member is unbonding, mapped from era index to the number of + /// points scheduled to unbond in the given era. + pub unbonding_eras: BoundedBTreeMap, T::MaxUnbonding>, +} + +impl PoolMember { + /// Inactive points of the member, waiting to be withdrawn. + pub fn unbonding_points(&self) -> BalanceOf { + self.unbonding_eras + .as_ref() + .iter() + .fold(BalanceOf::::zero(), |acc, (_, v)| acc.saturating_add(*v)) + } + + /// Withdraw any funds in [`Self::unbonding_eras`] who's deadline in reached and is fully + /// unlocked. + /// + /// Returns a a subset of [`Self::unbonding_eras`] that got withdrawn. + /// + /// Infallible, noop if no unbonding eras exist. + pub fn withdraw_unlocked( + &mut self, + current_era: EraIndex, + ) -> BoundedBTreeMap, T::MaxUnbonding> { + // NOTE: if only drain-filter was stable.. + let mut removed_points = + BoundedBTreeMap::, T::MaxUnbonding>::default(); + self.unbonding_eras.retain(|e, p| { + if *e > current_era { + true + } else { + removed_points + .try_insert(*e, *p) + .expect("source map is bounded, this is a subset, will be bounded; qed"); + false + } + }); + removed_points + } +} + +/// A pool's possible states. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, RuntimeDebugNoBound, Clone, Copy)] +pub enum PoolState { + /// The pool is open to be joined, and is working normally. + Open, + /// The pool is blocked. No one else can join. + Blocked, + /// The pool is in the process of being destroyed. + /// + /// All members can now be permissionlessly unbonded, and the pool can never go back to any + /// other state other than being dissolved. + Destroying, +} + +/// Pool administration roles. +/// +/// Any pool has a depositor, which can never change. But, all the other roles are optional, and +/// cannot exist. Note that if `root` is set to `None`, it basically means that the roles of this +/// pool can never change again (except via governance). +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Clone)] +pub struct PoolRoles { + /// Creates the pool and is the initial member. They can only leave the pool once all other + /// members have left. Once they fully leave, the pool is destroyed. + pub depositor: AccountId, + /// Can change the nominator, bouncer, or itself and can perform any of the actions the + /// nominator or bouncer can. + pub root: Option, + /// Can select which validators the pool nominates. + pub nominator: Option, + /// Can change the pools state and kick members if the pool is blocked. + pub bouncer: Option, +} diff --git a/pallets/tangle-lst/src/types/sub_pools.rs b/pallets/tangle-lst/src/types/sub_pools.rs new file mode 100644 index 000000000..c0f5852be --- /dev/null +++ b/pallets/tangle-lst/src/types/sub_pools.rs @@ -0,0 +1,271 @@ +use super::*; + +/// A reward pool. +/// +/// A reward pool is not so much a pool anymore, since it does not contain any shares or points. +/// Rather, simply to fit nicely next to bonded pool and unbonding pools in terms of terminology. In +/// reality, a reward pool is just a container for a few pool-dependent data related to the rewards. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, DefaultNoBound))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct RewardPool { + /// The last recorded value of the reward counter. + /// + /// This is updated ONLY when the points in the bonded pool change, which means `join`, + /// `bond_extra` and `unbond`, all of which is done through `update_recorded`. + pub last_recorded_reward_counter: T::RewardCounter, + /// The last recorded total payouts of the reward pool. + /// + /// Payouts is essentially income of the pool. + /// + /// Update criteria is same as that of `last_recorded_reward_counter`. + pub last_recorded_total_payouts: BalanceOf, + /// Total amount that this pool has paid out so far to the members. + pub total_rewards_claimed: BalanceOf, + /// The amount of commission pending to be claimed. + pub total_commission_pending: BalanceOf, + /// The amount of commission that has been claimed. + pub total_commission_claimed: BalanceOf, +} + +impl RewardPool { + /// Getter for [`RewardPool::last_recorded_reward_counter`]. + pub fn last_recorded_reward_counter(&self) -> T::RewardCounter { + self.last_recorded_reward_counter + } + + /// Register some rewards that are claimed from the pool by the members. + pub fn register_claimed_reward(&mut self, reward: BalanceOf) { + self.total_rewards_claimed = self.total_rewards_claimed.saturating_add(reward); + } + + /// Update the recorded values of the reward pool. + /// + /// This function MUST be called whenever the points in the bonded pool change, AND whenever the + /// the pools commission is updated. The reason for the former is that a change in pool points + /// will alter the share of the reward balance among pool members, and the reason for the latter + /// is that a change in commission will alter the share of the reward balance among the pool. + pub fn update_records( + &mut self, + id: PoolId, + bonded_points: BalanceOf, + commission: Perbill, + ) -> Result<(), Error> { + let balance = Self::current_balance(id); + + let (current_reward_counter, new_pending_commission) = + self.current_reward_counter(id, bonded_points, commission)?; + + // Store the reward counter at the time of this update. This is used in subsequent calls to + // `current_reward_counter`, whereby newly pending rewards (in points) are added to this + // value. + self.last_recorded_reward_counter = current_reward_counter; + + // Add any new pending commission that has been calculated from `current_reward_counter` to + // determine the total pending commission at the time of this update. + self.total_commission_pending = + self.total_commission_pending.saturating_add(new_pending_commission); + + // Total payouts are essentially the entire historical balance of the reward pool, equating + // to the current balance + the total rewards that have left the pool + the total commission + // that has left the pool. + let last_recorded_total_payouts = balance + .checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed)) + .ok_or(Error::::OverflowRisk)?; + + // Store the total payouts at the time of this update. + // + // An increase in ED could cause `last_recorded_total_payouts` to decrease but we should not + // allow that to happen since an already paid out reward cannot decrease. The reward account + // might go in deficit temporarily in this exceptional case but it will be corrected once + // new rewards are added to the pool. + self.last_recorded_total_payouts = + self.last_recorded_total_payouts.max(last_recorded_total_payouts); + + Ok(()) + } + + /// Get the current reward counter, based on the given `bonded_points` being the state of the + /// bonded pool at this time. + pub fn current_reward_counter( + &self, + id: PoolId, + bonded_points: BalanceOf, + commission: Perbill, + ) -> Result<(T::RewardCounter, BalanceOf), Error> { + let balance = Self::current_balance(id); + + // Calculate the current payout balance. The first 3 values of this calculation added + // together represent what the balance would be if no payouts were made. The + // `last_recorded_total_payouts` is then subtracted from this value to cancel out previously + // recorded payouts, leaving only the remaining payouts that have not been claimed. + let current_payout_balance = balance + .saturating_add(self.total_rewards_claimed) + .saturating_add(self.total_commission_claimed) + .saturating_sub(self.last_recorded_total_payouts); + + // Split the `current_payout_balance` into claimable rewards and claimable commission + // according to the current commission rate. + let new_pending_commission = commission * current_payout_balance; + let new_pending_rewards = current_payout_balance.saturating_sub(new_pending_commission); + + // * accuracy notes regarding the multiplication in `checked_from_rational`: + // `current_payout_balance` is a subset of the total_issuance at the very worse. + // `bonded_points` are similarly, in a non-slashed pool, have the same granularity as + // balance, and are thus below within the range of total_issuance. In the worse case + // scenario, for `saturating_from_rational`, we have: + // + // dot_total_issuance * 10^18 / `minJoinBond` + // + // assuming `MinJoinBond == ED` + // + // dot_total_issuance * 10^18 / 10^10 = dot_total_issuance * 10^8 + // + // which, with the current numbers, is a miniscule fraction of the u128 capacity. + // + // Thus, adding two values of type reward counter should be safe for ages in a chain like + // Polkadot. The important note here is that `reward_pool.last_recorded_reward_counter` only + // ever accumulates, but its semantics imply that it is less than total_issuance, when + // represented as `FixedU128`, which means it is less than `total_issuance * 10^18`. + // + // * accuracy notes regarding `checked_from_rational` collapsing to zero, meaning that no + // reward can be claimed: + // + // largest `bonded_points`, such that the reward counter is non-zero, with `FixedU128` will + // be when the payout is being computed. This essentially means `payout/bonded_points` needs + // to be more than 1/1^18. Thus, assuming that `bonded_points` will always be less than `10 + // * dot_total_issuance`, if the reward_counter is the smallest possible value, the value of + // the + // reward being calculated is: + // + // x / 10^20 = 1/ 10^18 + // + // x = 100 + // + // which is basically 10^-8 DOTs. See `smallest_claimable_reward` for an example of this. + let current_reward_counter = + T::RewardCounter::checked_from_rational(new_pending_rewards, bonded_points) + .and_then(|ref r| self.last_recorded_reward_counter.checked_add(r)) + .ok_or(Error::::OverflowRisk)?; + + Ok((current_reward_counter, new_pending_commission)) + } + + /// Current free balance of the reward pool. + /// + /// This is sum of all the rewards that are claimable by pool members. + pub fn current_balance(id: PoolId) -> BalanceOf { + T::Currency::free_balance(&Pallet::::create_reward_account(id)) + } +} + +/// An unbonding pool. This is always mapped with an era. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DefaultNoBound, RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct UnbondPool { + /// The points in this pool. + pub points: BalanceOf, + /// The funds in the pool. + pub balance: BalanceOf, +} + +impl UnbondPool { + pub fn balance_to_point(&self, new_funds: BalanceOf) -> BalanceOf { + Pallet::::balance_to_point(self.balance, self.points, new_funds) + } + + pub fn point_to_balance(&self, points: BalanceOf) -> BalanceOf { + Pallet::::point_to_balance(self.balance, self.points, points) + } + + /// Issue the equivalent points of `new_funds` into self. + /// + /// Returns the actual amounts of points issued. + pub fn issue(&mut self, new_funds: BalanceOf) -> BalanceOf { + let new_points = self.balance_to_point(new_funds); + self.points = self.points.saturating_add(new_points); + self.balance = self.balance.saturating_add(new_funds); + new_points + } + + /// Dissolve some points from the unbonding pool, reducing the balance of the pool + /// proportionally. This is the opposite of `issue`. + /// + /// Returns the actual amount of `Balance` that was removed from the pool. + pub fn dissolve(&mut self, points: BalanceOf) -> BalanceOf { + let balance_to_unbond = self.point_to_balance(points); + self.points = self.points.saturating_sub(points); + self.balance = self.balance.saturating_sub(balance_to_unbond); + + balance_to_unbond + } +} + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DefaultNoBound, RuntimeDebugNoBound)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq))] +#[codec(mel_bound(T: Config))] +#[scale_info(skip_type_params(T))] +pub struct SubPools { + /// A general, era agnostic pool of funds that have fully unbonded. The pools + /// of `Self::with_era` will lazily be merged into into this pool if they are + /// older then `current_era - TotalUnbondingPools`. + pub no_era: UnbondPool, + /// Map of era in which a pool becomes unbonded in => unbond pools. + pub with_era: BoundedBTreeMap, TotalUnbondingPools>, +} + +impl SubPools { + /// Merge the oldest `with_era` unbond pools into the `no_era` unbond pool. + /// + /// This is often used whilst getting the sub-pool from storage, thus it consumes and returns + /// `Self` for ergonomic purposes. + pub fn maybe_merge_pools(mut self, current_era: EraIndex) -> Self { + // Ex: if `TotalUnbondingPools` is 5 and current era is 10, we only want to retain pools + // 6..=10. Note that in the first few eras where `checked_sub` is `None`, we don't remove + // anything. + if let Some(newest_era_to_remove) = + current_era.checked_sub(T::PostUnbondingPoolsWindow::get()) + { + self.with_era.retain(|k, v| { + if *k > newest_era_to_remove { + // keep + true + } else { + // merge into the no-era pool + self.no_era.points = self.no_era.points.saturating_add(v.points); + self.no_era.balance = self.no_era.balance.saturating_add(v.balance); + false + } + }); + } + + self + } + + /// The sum of all unbonding balance, regardless of whether they are actually unlocked or not. + #[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))] + pub fn sum_unbonding_balance(&self) -> BalanceOf { + self.no_era.balance.saturating_add( + self.with_era + .values() + .fold(BalanceOf::::zero(), |acc, pool| acc.saturating_add(pool.balance)), + ) + } +} + +/// The maximum amount of eras an unbonding pool can exist prior to being merged with the +/// `no_era` pool. This is guaranteed to at least be equal to the staking `UnbondingDuration`. For +/// improved UX [`Config::PostUnbondingPoolsWindow`] should be configured to a non-zero value. +pub struct TotalUnbondingPools(PhantomData); + +impl Get for TotalUnbondingPools { + fn get() -> u32 { + // NOTE: this may be dangerous in the scenario bonding_duration gets decreased because + // we would no longer be able to decode `BoundedBTreeMap::, + // TotalUnbondingPools>`, which uses `TotalUnbondingPools` as the bound + T::Staking::bonding_duration() + T::PostUnbondingPoolsWindow::get() + } +} diff --git a/pallets/tangle-lst/src/weights.rs b/pallets/tangle-lst/src/weights.rs new file mode 100644 index 000000000..303db33b8 --- /dev/null +++ b/pallets/tangle-lst/src/weights.rs @@ -0,0 +1,1210 @@ +// This file is part of Substrate. + + + +//! Autogenerated weights for `pallet_nomination_pools` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-11-23, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-yprdrvc7-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/production/substrate-node +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_nomination_pools +// --chain=dev +// --header=./substrate/HEADER-APACHE2 +// --output=./substrate/frame/nomination-pools/src/weights.rs +// --template=./substrate/.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_nomination_pools`. +pub trait WeightInfo { + fn join() -> Weight; + fn bond_extra_transfer() -> Weight; + fn bond_extra_other() -> Weight; + fn claim_payout() -> Weight; + fn unbond() -> Weight; + fn pool_withdraw_unbonded(s: u32, ) -> Weight; + fn withdraw_unbonded_update(s: u32, ) -> Weight; + fn withdraw_unbonded_kill(s: u32, ) -> Weight; + fn create() -> Weight; + fn nominate(n: u32, ) -> Weight; + fn set_state() -> Weight; + fn set_metadata(n: u32, ) -> Weight; + fn set_configs() -> Weight; + fn update_roles() -> Weight; + fn chill() -> Weight; + fn set_commission() -> Weight; + fn set_commission_max() -> Weight; + fn set_commission_change_rate() -> Weight; + fn set_commission_claim_permission() -> Weight; + fn set_claim_permission() -> Weight; + fn claim_commission() -> Weight; + fn adjust_pool_deposit() -> Weight; +} + +/// Weights for `pallet_nomination_pools` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `NominationPools::MinJoinBond` (r:1 w:0) + /// Proof: `NominationPools::MinJoinBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembersPerPool` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembersPerPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembers` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + fn join() -> Weight { + // Proof Size summary in bytes: + // Measured: `3425` + // Estimated: `8877` + // Minimum execution time: 184_295_000 picoseconds. + Weight::from_parts(188_860_000, 8877) + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + fn bond_extra_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `3435` + // Estimated: `8877` + // Minimum execution time: 188_777_000 picoseconds. + Weight::from_parts(192_646_000, 8877) + .saturating_add(T::DbWeight::get().reads(17_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) + } + /// Storage: `NominationPools::ClaimPermissions` (r:1 w:0) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + fn bond_extra_other() -> Weight { + // Proof Size summary in bytes: + // Measured: `3500` + // Estimated: `8877` + // Minimum execution time: 221_728_000 picoseconds. + Weight::from_parts(227_569_000, 8877) + .saturating_add(T::DbWeight::get().reads(18_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)) + } + /// Storage: `NominationPools::ClaimPermissions` (r:1 w:0) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn claim_payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `1172` + // Estimated: `3719` + // Minimum execution time: 75_310_000 picoseconds. + Weight::from_parts(77_709_000, 3719) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:0) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `Staking::MinNominatorBond` (r:1 w:0) + /// Proof: `Staking::MinNominatorBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::SubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::SubPoolsStorage` (`max_values`: None, `max_size`: Some(24382), added: 26857, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForSubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::CounterForSubPoolsStorage` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn unbond() -> Weight { + // Proof Size summary in bytes: + // Measured: `3622` + // Estimated: `27847` + // Minimum execution time: 170_656_000 picoseconds. + Weight::from_parts(174_950_000, 27847) + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// The range of component `s` is `[0, 100]`. + fn pool_withdraw_unbonded(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1817` + // Estimated: `4764` + // Minimum execution time: 68_866_000 picoseconds. + Weight::from_parts(72_312_887, 4764) + // Standard Error: 1_635 + .saturating_add(Weight::from_parts(41_679, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::SubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::SubPoolsStorage` (`max_values`: None, `max_size`: Some(24382), added: 26857, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ClaimPermissions` (r:0 w:1) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_update(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2207` + // Estimated: `27847` + // Minimum execution time: 131_383_000 picoseconds. + Weight::from_parts(136_595_971, 27847) + // Standard Error: 2_715 + .saturating_add(Weight::from_parts(52_351, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(9_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::SubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::SubPoolsStorage` (`max_values`: None, `max_size`: Some(24382), added: 26857, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:1) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::SlashingSpans` (r:1 w:0) + /// Proof: `Staking::SlashingSpans` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:2 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:2 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Validators` (r:1 w:0) + /// Proof: `Staking::Validators` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:0) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::CounterForReversePoolIdLookup` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForRewardPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForRewardPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForSubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::CounterForSubPoolsStorage` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::Metadata` (r:1 w:1) + /// Proof: `NominationPools::Metadata` (`max_values`: None, `max_size`: Some(270), added: 2745, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForBondedPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForBondedPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::Payee` (r:0 w:1) + /// Proof: `Staking::Payee` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ClaimPermissions` (r:0 w:1) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_kill(_s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2525` + // Estimated: `27847` + // Minimum execution time: 233_314_000 picoseconds. + Weight::from_parts(241_694_316, 27847) + .saturating_add(T::DbWeight::get().reads(24_u64)) + .saturating_add(T::DbWeight::get().writes(20_u64)) + } + /// Storage: `NominationPools::LastPoolId` (r:1 w:1) + /// Proof: `NominationPools::LastPoolId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::MinNominatorBond` (r:1 w:0) + /// Proof: `Staking::MinNominatorBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MinCreateBond` (r:1 w:0) + /// Proof: `NominationPools::MinCreateBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MinJoinBond` (r:1 w:0) + /// Proof: `NominationPools::MinJoinBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPools` (r:1 w:0) + /// Proof: `NominationPools::MaxPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForBondedPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForBondedPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembersPerPool` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembersPerPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembers` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:1) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:2 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:2 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForRewardPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForRewardPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::CounterForReversePoolIdLookup` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:0 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::Payee` (r:0 w:1) + /// Proof: `Staking::Payee` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `1169` + // Estimated: `8538` + // Minimum execution time: 171_465_000 picoseconds. + Weight::from_parts(176_478_000, 8538) + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(17_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:0) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::MinNominatorBond` (r:1 w:0) + /// Proof: `Staking::MinNominatorBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:1) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `Staking::MaxNominatorsCount` (r:1 w:0) + /// Proof: `Staking::MaxNominatorsCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::Validators` (r:17 w:0) + /// Proof: `Staking::Validators` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:1 w:1) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:1 w:1) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `VoterList::CounterForListNodes` (r:1 w:1) + /// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::CounterForNominators` (r:1 w:1) + /// Proof: `Staking::CounterForNominators` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 16]`. + fn nominate(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1808` + // Estimated: `4556 + n * (2520 ±0)` + // Minimum execution time: 63_588_000 picoseconds. + Weight::from_parts(64_930_584, 4556) + // Standard Error: 9_167 + .saturating_add(Weight::from_parts(1_595_779, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(12_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(5_u64)) + .saturating_add(Weight::from_parts(0, 2520).saturating_mul(n.into())) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:0) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + fn set_state() -> Weight { + // Proof Size summary in bytes: + // Measured: `1434` + // Estimated: `4556` + // Minimum execution time: 32_899_000 picoseconds. + Weight::from_parts(33_955_000, 4556) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::Metadata` (r:1 w:1) + /// Proof: `NominationPools::Metadata` (`max_values`: None, `max_size`: Some(270), added: 2745, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForMetadata` (r:1 w:1) + /// Proof: `NominationPools::CounterForMetadata` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 256]`. + fn set_metadata(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3735` + // Minimum execution time: 13_778_000 picoseconds. + Weight::from_parts(14_770_006, 3735) + // Standard Error: 151 + .saturating_add(Weight::from_parts(1_900, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `NominationPools::MinJoinBond` (r:0 w:1) + /// Proof: `NominationPools::MinJoinBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembers` (r:0 w:1) + /// Proof: `NominationPools::MaxPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembersPerPool` (r:0 w:1) + /// Proof: `NominationPools::MaxPoolMembersPerPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MinCreateBond` (r:0 w:1) + /// Proof: `NominationPools::MinCreateBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:0 w:1) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPools` (r:0 w:1) + /// Proof: `NominationPools::MaxPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn set_configs() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 4_550_000 picoseconds. + Weight::from_parts(4_935_000, 0) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + fn update_roles() -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3719` + // Minimum execution time: 16_759_000 picoseconds. + Weight::from_parts(17_346_000, 3719) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:0) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::Validators` (r:1 w:0) + /// Proof: `Staking::Validators` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:1) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `Staking::CounterForNominators` (r:1 w:1) + /// Proof: `Staking::CounterForNominators` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:1 w:1) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:1 w:1) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `VoterList::CounterForListNodes` (r:1 w:1) + /// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn chill() -> Weight { + // Proof Size summary in bytes: + // Measured: `1971` + // Estimated: `4556` + // Minimum execution time: 61_970_000 picoseconds. + Weight::from_parts(63_738_000, 4556) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn set_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `804` + // Estimated: `3719` + // Minimum execution time: 31_950_000 picoseconds. + Weight::from_parts(33_190_000, 3719) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn set_commission_max() -> Weight { + // Proof Size summary in bytes: + // Measured: `572` + // Estimated: `3719` + // Minimum execution time: 16_807_000 picoseconds. + Weight::from_parts(17_733_000, 3719) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + fn set_commission_change_rate() -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3719` + // Minimum execution time: 16_710_000 picoseconds. + Weight::from_parts(17_563_000, 3719) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + fn set_commission_claim_permission() -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3719` + // Minimum execution time: 16_493_000 picoseconds. + Weight::from_parts(17_022_000, 3719) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:0) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ClaimPermissions` (r:1 w:1) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + fn set_claim_permission() -> Weight { + // Proof Size summary in bytes: + // Measured: `542` + // Estimated: `3702` + // Minimum execution time: 14_248_000 picoseconds. + Weight::from_parts(15_095_000, 3702) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn claim_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `1002` + // Estimated: `3719` + // Minimum execution time: 61_969_000 picoseconds. + Weight::from_parts(63_965_000, 3719) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:0) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + fn adjust_pool_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `901` + // Estimated: `4764` + // Minimum execution time: 65_462_000 picoseconds. + Weight::from_parts(67_250_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `NominationPools::MinJoinBond` (r:1 w:0) + /// Proof: `NominationPools::MinJoinBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembersPerPool` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembersPerPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembers` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + fn join() -> Weight { + // Proof Size summary in bytes: + // Measured: `3425` + // Estimated: `8877` + // Minimum execution time: 184_295_000 picoseconds. + Weight::from_parts(188_860_000, 8877) + .saturating_add(RocksDbWeight::get().reads(20_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + fn bond_extra_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `3435` + // Estimated: `8877` + // Minimum execution time: 188_777_000 picoseconds. + Weight::from_parts(192_646_000, 8877) + .saturating_add(RocksDbWeight::get().reads(17_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) + } + /// Storage: `NominationPools::ClaimPermissions` (r:1 w:0) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + fn bond_extra_other() -> Weight { + // Proof Size summary in bytes: + // Measured: `3500` + // Estimated: `8877` + // Minimum execution time: 221_728_000 picoseconds. + Weight::from_parts(227_569_000, 8877) + .saturating_add(RocksDbWeight::get().reads(18_u64)) + .saturating_add(RocksDbWeight::get().writes(14_u64)) + } + /// Storage: `NominationPools::ClaimPermissions` (r:1 w:0) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn claim_payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `1172` + // Estimated: `3719` + // Minimum execution time: 75_310_000 picoseconds. + Weight::from_parts(77_709_000, 3719) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:0) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `Staking::MinNominatorBond` (r:1 w:0) + /// Proof: `Staking::MinNominatorBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:3 w:3) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:2 w:2) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::SubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::SubPoolsStorage` (`max_values`: None, `max_size`: Some(24382), added: 26857, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForSubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::CounterForSubPoolsStorage` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn unbond() -> Weight { + // Proof Size summary in bytes: + // Measured: `3622` + // Estimated: `27847` + // Minimum execution time: 170_656_000 picoseconds. + Weight::from_parts(174_950_000, 27847) + .saturating_add(RocksDbWeight::get().reads(20_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// The range of component `s` is `[0, 100]`. + fn pool_withdraw_unbonded(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1817` + // Estimated: `4764` + // Minimum execution time: 68_866_000 picoseconds. + Weight::from_parts(72_312_887, 4764) + // Standard Error: 1_635 + .saturating_add(Weight::from_parts(41_679, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::SubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::SubPoolsStorage` (`max_values`: None, `max_size`: Some(24382), added: 26857, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ClaimPermissions` (r:0 w:1) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_update(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2207` + // Estimated: `27847` + // Minimum execution time: 131_383_000 picoseconds. + Weight::from_parts(136_595_971, 27847) + // Standard Error: 2_715 + .saturating_add(Weight::from_parts(52_351, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(11_u64)) + .saturating_add(RocksDbWeight::get().writes(9_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::SubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::SubPoolsStorage` (`max_values`: None, `max_size`: Some(24382), added: 26857, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:1) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::SlashingSpans` (r:1 w:0) + /// Proof: `Staking::SlashingSpans` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:2 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:2 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Validators` (r:1 w:0) + /// Proof: `Staking::Validators` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:0) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::CounterForReversePoolIdLookup` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForRewardPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForRewardPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForSubPoolsStorage` (r:1 w:1) + /// Proof: `NominationPools::CounterForSubPoolsStorage` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::Metadata` (r:1 w:1) + /// Proof: `NominationPools::Metadata` (`max_values`: None, `max_size`: Some(270), added: 2745, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForBondedPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForBondedPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::Payee` (r:0 w:1) + /// Proof: `Staking::Payee` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ClaimPermissions` (r:0 w:1) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// The range of component `s` is `[0, 100]`. + fn withdraw_unbonded_kill(_s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2525` + // Estimated: `27847` + // Minimum execution time: 233_314_000 picoseconds. + Weight::from_parts(241_694_316, 27847) + .saturating_add(RocksDbWeight::get().reads(24_u64)) + .saturating_add(RocksDbWeight::get().writes(20_u64)) + } + /// Storage: `NominationPools::LastPoolId` (r:1 w:1) + /// Proof: `NominationPools::LastPoolId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::MinNominatorBond` (r:1 w:0) + /// Proof: `Staking::MinNominatorBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MinCreateBond` (r:1 w:0) + /// Proof: `NominationPools::MinCreateBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MinJoinBond` (r:1 w:0) + /// Proof: `NominationPools::MinJoinBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPools` (r:1 w:0) + /// Proof: `NominationPools::MaxPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForBondedPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForBondedPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::PoolMembers` (r:1 w:1) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembersPerPool` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembersPerPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembers` (r:1 w:0) + /// Proof: `NominationPools::MaxPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForPoolMembers` (r:1 w:1) + /// Proof: `NominationPools::CounterForPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:1) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:2 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:2 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::TotalValueLocked` (r:1 w:1) + /// Proof: `NominationPools::TotalValueLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForRewardPools` (r:1 w:1) + /// Proof: `NominationPools::CounterForRewardPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::ReversePoolIdLookup` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForReversePoolIdLookup` (r:1 w:1) + /// Proof: `NominationPools::CounterForReversePoolIdLookup` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:0 w:1) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::Payee` (r:0 w:1) + /// Proof: `Staking::Payee` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `1169` + // Estimated: `8538` + // Minimum execution time: 171_465_000 picoseconds. + Weight::from_parts(176_478_000, 8538) + .saturating_add(RocksDbWeight::get().reads(23_u64)) + .saturating_add(RocksDbWeight::get().writes(17_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:0) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::MinNominatorBond` (r:1 w:0) + /// Proof: `Staking::MinNominatorBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:1) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `Staking::MaxNominatorsCount` (r:1 w:0) + /// Proof: `Staking::MaxNominatorsCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::Validators` (r:17 w:0) + /// Proof: `Staking::Validators` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Staking::CurrentEra` (r:1 w:0) + /// Proof: `Staking::CurrentEra` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:1 w:1) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:1 w:1) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `VoterList::CounterForListNodes` (r:1 w:1) + /// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Staking::CounterForNominators` (r:1 w:1) + /// Proof: `Staking::CounterForNominators` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 16]`. + fn nominate(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1808` + // Estimated: `4556 + n * (2520 ±0)` + // Minimum execution time: 63_588_000 picoseconds. + Weight::from_parts(64_930_584, 4556) + // Standard Error: 9_167 + .saturating_add(Weight::from_parts(1_595_779, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(12_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + .saturating_add(Weight::from_parts(0, 2520).saturating_mul(n.into())) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:0) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + fn set_state() -> Weight { + // Proof Size summary in bytes: + // Measured: `1434` + // Estimated: `4556` + // Minimum execution time: 32_899_000 picoseconds. + Weight::from_parts(33_955_000, 4556) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::Metadata` (r:1 w:1) + /// Proof: `NominationPools::Metadata` (`max_values`: None, `max_size`: Some(270), added: 2745, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::CounterForMetadata` (r:1 w:1) + /// Proof: `NominationPools::CounterForMetadata` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 256]`. + fn set_metadata(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3735` + // Minimum execution time: 13_778_000 picoseconds. + Weight::from_parts(14_770_006, 3735) + // Standard Error: 151 + .saturating_add(Weight::from_parts(1_900, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `NominationPools::MinJoinBond` (r:0 w:1) + /// Proof: `NominationPools::MinJoinBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembers` (r:0 w:1) + /// Proof: `NominationPools::MaxPoolMembers` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPoolMembersPerPool` (r:0 w:1) + /// Proof: `NominationPools::MaxPoolMembersPerPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MinCreateBond` (r:0 w:1) + /// Proof: `NominationPools::MinCreateBond` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:0 w:1) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::MaxPools` (r:0 w:1) + /// Proof: `NominationPools::MaxPools` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn set_configs() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 4_550_000 picoseconds. + Weight::from_parts(4_935_000, 0) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + fn update_roles() -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3719` + // Minimum execution time: 16_759_000 picoseconds. + Weight::from_parts(17_346_000, 3719) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Staking::Bonded` (r:1 w:0) + /// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`) + /// Storage: `Staking::Ledger` (r:1 w:0) + /// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(1091), added: 3566, mode: `MaxEncodedLen`) + /// Storage: `Staking::Validators` (r:1 w:0) + /// Proof: `Staking::Validators` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Staking::Nominators` (r:1 w:1) + /// Proof: `Staking::Nominators` (`max_values`: None, `max_size`: Some(558), added: 3033, mode: `MaxEncodedLen`) + /// Storage: `Staking::CounterForNominators` (r:1 w:1) + /// Proof: `Staking::CounterForNominators` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListNodes` (r:1 w:1) + /// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`) + /// Storage: `VoterList::ListBags` (r:1 w:1) + /// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`) + /// Storage: `VoterList::CounterForListNodes` (r:1 w:1) + /// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn chill() -> Weight { + // Proof Size summary in bytes: + // Measured: `1971` + // Estimated: `4556` + // Minimum execution time: 61_970_000 picoseconds. + Weight::from_parts(63_738_000, 4556) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn set_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `804` + // Estimated: `3719` + // Minimum execution time: 31_950_000 picoseconds. + Weight::from_parts(33_190_000, 3719) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn set_commission_max() -> Weight { + // Proof Size summary in bytes: + // Measured: `572` + // Estimated: `3719` + // Minimum execution time: 16_807_000 picoseconds. + Weight::from_parts(17_733_000, 3719) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + fn set_commission_change_rate() -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3719` + // Minimum execution time: 16_710_000 picoseconds. + Weight::from_parts(17_563_000, 3719) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:1) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + fn set_commission_claim_permission() -> Weight { + // Proof Size summary in bytes: + // Measured: `532` + // Estimated: `3719` + // Minimum execution time: 16_493_000 picoseconds. + Weight::from_parts(17_022_000, 3719) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::PoolMembers` (r:1 w:0) + /// Proof: `NominationPools::PoolMembers` (`max_values`: None, `max_size`: Some(237), added: 2712, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::ClaimPermissions` (r:1 w:1) + /// Proof: `NominationPools::ClaimPermissions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + fn set_claim_permission() -> Weight { + // Proof Size summary in bytes: + // Measured: `542` + // Estimated: `3702` + // Minimum execution time: 14_248_000 picoseconds. + Weight::from_parts(15_095_000, 3702) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::RewardPools` (r:1 w:1) + /// Proof: `NominationPools::RewardPools` (`max_values`: None, `max_size`: Some(92), added: 2567, mode: `MaxEncodedLen`) + /// Storage: `NominationPools::GlobalMaxCommission` (r:1 w:0) + /// Proof: `NominationPools::GlobalMaxCommission` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn claim_commission() -> Weight { + // Proof Size summary in bytes: + // Measured: `1002` + // Estimated: `3719` + // Minimum execution time: 61_969_000 picoseconds. + Weight::from_parts(63_965_000, 3719) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `NominationPools::BondedPools` (r:1 w:0) + /// Proof: `NominationPools::BondedPools` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:0) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + fn adjust_pool_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `901` + // Estimated: `4764` + // Minimum execution time: 65_462_000 picoseconds. + Weight::from_parts(67_250_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } +} diff --git a/pallets/tangle-lst/test-staking/Cargo.toml b/pallets/tangle-lst/test-staking/Cargo.toml new file mode 100644 index 000000000..68cc5e503 --- /dev/null +++ b/pallets/tangle-lst/test-staking/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "pallet-tangle-lst-test-staking" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +# FRAME +frame-benchmarking = { workspace = true, optional = true } +frame-election-provider-support = { workspace = true, optional = true } +frame-support = { workspace = true, optional = true } +frame-system = { workspace = true, optional = true } +log = { workspace = true, optional = true } +pallet-bags-list = { workspace = true, optional = true } +pallet-balances = { workspace = true, optional = true } +pallet-nomination-pools = { workspace = true, optional = true } +pallet-staking = { workspace = true, optional = true } +parity-scale-codec = { workspace = true, default-features = true } +cfg-if = "1.0" +smart-default = "0.6.0" + +# Substrate Primitives +sp-runtime = { workspace = true, optional = true } +sp-staking = { workspace = true, optional = true } +sp-std = { workspace = true, optional = true } + +[dev-dependencies] +frame-election-provider-support = { workspace = true, default-features = true } +frame-support = { workspace = true, default-features = true } +frame-system = { workspace = true, default-features = true } +pallet-bags-list = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +pallet-tangle-lst = { workspace = true, default-features = true } +pallet-session = { workspace = true, default-features = true } +pallet-staking = { workspace = true, default-features = true } +pallet-staking-reward-curve = { workspace = true, default-features = true } +pallet-timestamp = { workspace = true, default-features = true } + +scale-info = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } +sp-staking = { workspace = true, default-features = true } +sp-std = { workspace = true, default-features = true } +sp-tracing = { workspace = true, default-features = true } + +[features] +default = ["std"] + +std = [ + "frame-benchmarking?/std", + "frame-election-provider-support?/std", + "frame-support?/std", + "frame-system?/std", + "pallet-bags-list?/std", + "pallet-staking?/std", + "pallet-nomination-pools?/std", + "sp-runtime?/std", + "sp-staking?/std", + "sp-std?/std", +] + +runtime-benchmarks = [ + "frame-benchmarking", + "frame-election-provider-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-staking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "pallet-staking/runtime-benchmarks", + "pallet-nomination-pools/runtime-benchmarks", + "pallet-bags-list/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "sp-std", + "log", +] + +try-runtime = [ + "frame-system?/try-runtime", + "sp-runtime?/try-runtime", + "frame-support?/try-runtime", + "pallet-staking?/try-runtime", + "pallet-nomination-pools?/try-runtime", + "pallet-bags-list?/try-runtime", + "pallet-balances?/try-runtime", + "pallet-timestamp/try-runtime", + "frame-election-provider-support/try-runtime", +] \ No newline at end of file diff --git a/pallets/tangle-lst/test-staking/src/lib.rs b/pallets/tangle-lst/test-staking/src/lib.rs new file mode 100644 index 000000000..af8e5f34f --- /dev/null +++ b/pallets/tangle-lst/test-staking/src/lib.rs @@ -0,0 +1,683 @@ +// This file is part of Substrate. + + + +#![cfg(test)] + +mod mock; + +use frame_support::{assert_noop, assert_ok, traits::Currency}; +use mock::*; +use pallet_nomination_pools::{ + BondedPools, Error as PoolsError, Event as PoolsEvent, LastPoolId, PoolMember, PoolMembers, + PoolState, +}; +use pallet_staking::{CurrentEra, Event as StakingEvent, Payee, RewardDestination}; +use sp_runtime::{bounded_btree_map, traits::Zero}; + +#[test] +fn pool_lifecycle_e2e() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::minimum_balance(), 5); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 50, 10, 10, 10)); + assert_eq!(LastPoolId::::get(), 1); + + // have the pool nominate. + assert_ok!(Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3])); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 50 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 50, joined: true }, + ] + ); + + // have two members join + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(21), 10, 1)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, + PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 10, joined: true }, + ] + ); + + // pool goes into destroying + assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying)); + + // depositor cannot unbond yet. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + PoolsError::::MinimumBondNotMet, + ); + + // now the members want to unbond. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10)); + + assert_eq!(PoolMembers::::get(20).unwrap().unbonding_eras.len(), 1); + assert_eq!(PoolMembers::::get(20).unwrap().points, 0); + assert_eq!(PoolMembers::::get(21).unwrap().unbonding_eras.len(), 1); + assert_eq!(PoolMembers::::get(21).unwrap().points, 0); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + PoolsEvent::Unbonded { member: 20, pool_id: 1, points: 10, balance: 10, era: 3 }, + PoolsEvent::Unbonded { member: 21, pool_id: 1, points: 10, balance: 10, era: 3 }, + ] + ); + + // depositor cannot still unbond + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + PoolsError::::MinimumBondNotMet, + ); + + for e in 1..BondingDuration::get() { + CurrentEra::::set(Some(e)); + assert_noop!( + Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0), + PoolsError::::CannotWithdrawAny + ); + } + + // members are now unlocked. + CurrentEra::::set(Some(BondingDuration::get())); + + // depositor cannot still unbond + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + PoolsError::::MinimumBondNotMet, + ); + + // but members can now withdraw. + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0)); + assert!(PoolMembers::::get(20).is_none()); + assert!(PoolMembers::::get(21).is_none()); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 20 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Withdrawn { member: 20, pool_id: 1, points: 10, balance: 10 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 20 }, + PoolsEvent::Withdrawn { member: 21, pool_id: 1, points: 10, balance: 10 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 21 }, + ] + ); + + // as soon as all members have left, the depositor can try to unbond, but since the + // min-nominator intention is set, they must chill first. + assert_noop!( + Pools::unbond(RuntimeOrigin::signed(10), 10, 50), + pallet_staking::Error::::InsufficientBond + ); + + assert_ok!(Pools::chill(RuntimeOrigin::signed(10), 1)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 50)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Chilled { stash: POOL1_BONDED }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 50 }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { member: 10, pool_id: 1, points: 50, balance: 50, era: 6 }] + ); + + // waiting another bonding duration: + CurrentEra::::set(Some(BondingDuration::get() * 2)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 1)); + + // pools is fully destroyed now. + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 50 },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Withdrawn { member: 10, pool_id: 1, points: 50, balance: 50 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 10 }, + PoolsEvent::Destroyed { pool_id: 1 } + ] + ); + }) +} + +#[test] +fn pool_slash_e2e() { + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!(LastPoolId::::get(), 1); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + assert_eq!( + Payee::::get(POOL1_BONDED), + Some(RewardDestination::Account(POOL1_REWARD)) + ); + + // have two members join + assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(21), 20, 1)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 20 }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: 20 } + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, + PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 20, joined: true }, + ] + ); + + // now let's progress a bit. + CurrentEra::::set(Some(1)); + + // 20 / 80 of the total funds are unlocked, and safe from any further slash. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 } + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 }, + PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 4 } + ] + ); + + CurrentEra::::set(Some(2)); + + // note: depositor cannot fully unbond at this point. + // these funds will still get slashed. + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }, + ] + ); + + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 5 }, + PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 5 }, + PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: 10, points: 10, era: 5 }, + ] + ); + + // At this point, 20 are safe from slash, 30 are unlocking but vulnerable to slash, and and + // another 30 are active and vulnerable to slash. Let's slash half of them. + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 30, + &mut Default::default(), + &mut Default::default(), + 2, // slash era 2, affects chunks at era 5 onwards. + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 30 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + // 30 has been slashed to 15 (15 slash) + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 5, balance: 15 }, + // 30 has been slashed to 15 (15 slash) + PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 } + ] + ); + + CurrentEra::::set(Some(3)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10)); + + assert_eq!( + PoolMembers::::get(21).unwrap(), + PoolMember { + pool_id: 1, + points: 0, + last_recorded_reward_counter: Zero::zero(), + // the 10 points unlocked just now correspond to 5 points in the unbond pool. + unbonding_eras: bounded_btree_map!(5 => 10, 6 => 5) + } + ); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 5 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: 5, points: 5, era: 6 }] + ); + + // now we start withdrawing. we do it all at once, at era 6 where 20 and 21 are fully free. + CurrentEra::::set(Some(6)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0)); + + assert_eq!( + pool_events_since_last_call(), + vec![ + // 20 had unbonded 10 safely, and 10 got slashed by half. + PoolsEvent::Withdrawn { member: 20, pool_id: 1, balance: 10 + 5, points: 20 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 20 }, + // 21 unbonded all of it after the slash + PoolsEvent::Withdrawn { member: 21, pool_id: 1, balance: 5 + 5, points: 15 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 21 } + ] + ); + assert_eq!( + staking_events_since_last_call(), + // a 10 (un-slashed) + 10/2 (slashed) balance from 10 has also been unlocked + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 15 + 10 + 15 }] + ); + + // now, finally, we can unbond the depositor further than their current limit. + assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 20)); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, + PoolsEvent::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 } + ] + ); + + CurrentEra::::set(Some(9)); + assert_eq!( + PoolMembers::::get(10).unwrap(), + PoolMember { + pool_id: 1, + points: 0, + last_recorded_reward_counter: Zero::zero(), + unbonding_eras: bounded_btree_map!(4 => 10, 5 => 10, 9 => 10) + } + ); + // withdraw the depositor, they should lose 12 balance in total due to slash. + assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 10 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Withdrawn { member: 10, pool_id: 1, balance: 10 + 15, points: 30 }, + PoolsEvent::MemberRemoved { pool_id: 1, member: 10 }, + PoolsEvent::Destroyed { pool_id: 1 } + ] + ); + }); +} + +#[test] +fn pool_slash_proportional() { + // a typical example where 3 pool members unbond in era 99, 100, and 101, and a slash that + // happened in era 100 should only affect the latter two. + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + BondingDuration::set(28); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!(LastPoolId::::get(), 1); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + // have two members join + let bond = 20; + assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(21), bond, 1)); + assert_ok!(Pools::join(RuntimeOrigin::signed(22), bond, 1)); + + assert_eq!( + staking_events_since_last_call(), + vec![ + StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }, + StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }, + ] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }, + PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: bond, joined: true }, + PoolsEvent::Bonded { member: 22, pool_id: 1, bonded: bond, joined: true }, + ] + ); + + // now let's progress a lot. + CurrentEra::::set(Some(99)); + + // and unbond + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond)); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 20, + pool_id: 1, + balance: bond, + points: bond, + era: 127 + }] + ); + + CurrentEra::::set(Some(100)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 21, + pool_id: 1, + balance: bond, + points: bond, + era: 128 + }] + ); + + CurrentEra::::set(Some(101)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(22), 22, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 22, + pool_id: 1, + balance: bond, + points: bond, + era: 129 + }] + ); + + // Apply a slash that happened in era 100. This is typically applied with a delay. + // Of the total 100, 50 is slashed. + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 50, + &mut Default::default(), + &mut Default::default(), + 100, + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 50 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + // This era got slashed 12.5, which rounded up to 13. + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 128, balance: 7 }, + // This era got slashed 12 instead of 12.5 because an earlier chunk got 0.5 more + // slashed, and 12 is all the remaining slash + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 129, balance: 8 }, + // Bonded pool got slashed for 25, remaining 15 in it. + PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 } + ] + ); + }); +} + +#[test] +fn pool_slash_non_proportional_only_bonded_pool() { + // A typical example where a pool member unbonds in era 99, and they can get away with a slash + // that happened in era 100, as long as the pool has enough active bond to cover the slash. If + // everything else in the slashing/staking system works, this should always be the case. + // Nonetheless, `ledger.slash` has been written such that it will slash greedily from any chunk + // if it runs out of chunks that it thinks should be affected by the slash. + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + BondingDuration::set(28); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + // have two members join + let bond = 20; + assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }] + ); + + // progress and unbond. + CurrentEra::::set(Some(99)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 20, + pool_id: 1, + balance: bond, + points: bond, + era: 127 + }] + ); + + // slash for 30. This will be deducted only from the bonded pool. + CurrentEra::::set(Some(100)); + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 30, + &mut Default::default(), + &mut Default::default(), + 100, + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 30 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::PoolSlashed { pool_id: 1, balance: 10 }] + ); + }); +} + +#[test] +fn pool_slash_non_proportional_bonded_pool_and_chunks() { + // An uncommon example where even though some funds are unlocked such that they should not be + // affected by a slash, we still slash out of them. This should not happen at all. If a + // nomination has unbonded, from the next era onwards, their exposure will drop, so if an era + // happens in that era, then their share of that slash should naturally be less, such that only + // their active ledger stake is enough to compensate it. + new_test_ext().execute_with(|| { + ExistentialDeposit::set(1); + BondingDuration::set(28); + assert_eq!(Balances::minimum_balance(), 1); + assert_eq!(Staking::current_era(), None); + + // create the pool, we know this has id 1. + assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + PoolsEvent::Created { depositor: 10, pool_id: 1 }, + PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true }, + ] + ); + + // have two members join + let bond = 20; + assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }] + ); + + // progress and unbond. + CurrentEra::::set(Some(99)); + assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond)); + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![PoolsEvent::Unbonded { + member: 20, + pool_id: 1, + balance: bond, + points: bond, + era: 127 + }] + ); + + // slash 50. This will be deducted only from the bonded pool and one of the unbonding pools. + CurrentEra::::set(Some(100)); + assert_eq!(BondedPools::::get(1).unwrap().points, 40); + pallet_staking::slashing::do_slash::( + &POOL1_BONDED, + 50, + &mut Default::default(), + &mut Default::default(), + 100, + ); + + assert_eq!( + staking_events_since_last_call(), + vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 50 }] + ); + assert_eq!( + pool_events_since_last_call(), + vec![ + // out of 20, 10 was taken. + PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 127, balance: 10 }, + // out of 40, all was taken. + PoolsEvent::PoolSlashed { pool_id: 1, balance: 0 } + ] + ); + }); +} diff --git a/pallets/tangle-lst/test-staking/src/mock.rs b/pallets/tangle-lst/test-staking/src/mock.rs new file mode 100644 index 000000000..3beed33fb --- /dev/null +++ b/pallets/tangle-lst/test-staking/src/mock.rs @@ -0,0 +1,258 @@ +// This file is part of Substrate. + + + +use frame_election_provider_support::VoteWeight; +use frame_support::{ + assert_ok, derive_impl, + pallet_prelude::*, + parameter_types, + traits::{ConstU64, ConstU8}, + PalletId, +}; +use sp_runtime::{ + traits::{Convert, IdentityLookup}, + BuildStorage, FixedU128, Perbill, +}; + +type AccountId = u128; +type Nonce = u32; +type BlockNumber = u64; +type Balance = u128; + +pub(crate) type T = Runtime; + +pub(crate) const POOL1_BONDED: AccountId = 20318131474730217858575332831085u128; +pub(crate) const POOL1_REWARD: AccountId = 20397359637244482196168876781421u128; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = Nonce; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +parameter_types! { + pub static ExistentialDeposit: Balance = 5; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = ConstU32<1>; + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); +} + +pallet_staking_reward_curve::build! { + const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!( + min_inflation: 0_025_000, + max_inflation: 0_100_000, + ideal_stake: 0_500_000, + falloff: 0_050_000, + max_piece_count: 40, + test_precision: 0_005_000, + ); +} + +parameter_types! { + pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS; + pub static BondingDuration: u32 = 3; +} + +impl pallet_staking::Config for Runtime { + type Currency = Balances; + type CurrencyBalance = Balance; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type AdminOrigin = frame_system::EnsureRoot; + type BondingDuration = BondingDuration; + type SessionInterface = (); + type EraPayout = pallet_staking::ConvertCurve; + type NextNewSession = (); + type MaxExposurePageSize = ConstU32<64>; + type OffendingValidatorsThreshold = (); + type ElectionProvider = + frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = VoterList; + type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; + type MaxUnlockingChunks = ConstU32<32>; + type MaxControllersInDeprecationBatch = ConstU32<100>; + type HistoryDepth = ConstU32<84>; + type EventListeners = Pools; + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type WeightInfo = (); +} + +parameter_types! { + pub static BagThresholds: &'static [VoteWeight] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; +} + +type VoterBagsListInstance = pallet_bags_list::Instance1; +impl pallet_bags_list::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type BagThresholds = BagThresholds; + type ScoreProvider = Staking; + type Score = VoteWeight; +} + +pub struct BalanceToU256; +impl Convert for BalanceToU256 { + fn convert(n: Balance) -> sp_core::U256 { + n.into() + } +} + +pub struct U256ToBalance; +impl Convert for U256ToBalance { + fn convert(n: sp_core::U256) -> Balance { + n.try_into().unwrap() + } +} + +parameter_types! { + pub const PostUnbondingPoolsWindow: u32 = 10; + pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls"); +} + +impl pallet_nomination_pools::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RewardCounter = FixedU128; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = Staking; + type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow; + type MaxMetadataLen = ConstU32<256>; + type MaxUnbonding = ConstU32<8>; + type MaxPointsToBalance = ConstU8<10>; + type PalletId = PoolsPalletId; +} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Runtime { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + Staking: pallet_staking, + VoterList: pallet_bags_list::, + Pools: pallet_nomination_pools, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let _ = pallet_nomination_pools::GenesisConfig:: { + min_join_bond: 2, + min_create_bond: 2, + max_pools: Some(3), + max_members_per_pool: Some(5), + max_members: Some(3 * 5), + global_max_commission: Some(Perbill::from_percent(90)), + } + .assimilate_storage(&mut storage) + .unwrap(); + + let _ = pallet_balances::GenesisConfig:: { + balances: vec![(10, 100), (20, 100), (21, 100), (22, 100)], + } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext = sp_io::TestExternalities::from(storage); + + ext.execute_with(|| { + // for events to be deposited. + frame_system::Pallet::::set_block_number(1); + + // set some limit for nominations. + assert_ok!(Staking::set_staking_configs( + RuntimeOrigin::root(), + pallet_staking::ConfigOp::Set(10), // minimum nominator bond + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + pallet_staking::ConfigOp::Noop, + )); + }); + + ext +} + +parameter_types! { + static ObservedEventsPools: usize = 0; + static ObservedEventsStaking: usize = 0; + static ObservedEventsBalances: usize = 0; +} + +pub(crate) fn pool_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Pools(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = ObservedEventsPools::get(); + ObservedEventsPools::set(events.len()); + events.into_iter().skip(already_seen).collect() +} + +pub(crate) fn staking_events_since_last_call() -> Vec> { + let events = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::Staking(inner) = e { Some(inner) } else { None }) + .collect::>(); + let already_seen = ObservedEventsStaking::get(); + ObservedEventsStaking::set(events.len()); + events.into_iter().skip(already_seen).collect() +} diff --git a/precompiles/tangle-lst/Cargo.toml b/precompiles/tangle-lst/Cargo.toml new file mode 100644 index 000000000..f3baace8b --- /dev/null +++ b/precompiles/tangle-lst/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "pallet-evm-precompile-tangle-lst" +version = "0.1.0" +authors = { workspace = true } +edition = "2021" +description = "A Precompile to make pallet-multi-asset-delegation calls encoding accessible to pallet-evm" + +[dependencies] + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-multi-asset-delegation = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = ["forbid-evm-reentrancy"] } + +tangle-primitives = { workspace = true } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = ["std", "testing"] } + +# Substrate +pallet-balances = { workspace = true, features = ["std"] } +pallet-assets = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true, features = ["std"] } +scale-info = { workspace = true, features = ["derive", "std"] } +sp-io = { workspace = true, features = ["std"] } + +[features] +default = ["std"] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-multi-asset-delegation/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "tangle-primitives/std", + "pallet-assets/std" +] diff --git a/precompiles/tangle-lst/MultiAssetDelegation.sol b/precompiles/tangle-lst/MultiAssetDelegation.sol new file mode 100644 index 000000000..9c1531d77 --- /dev/null +++ b/precompiles/tangle-lst/MultiAssetDelegation.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The TangleLst contract's address. +address constant TANGLE_LST = 0x0000000000000000000000000000000000000809; + +/// @dev The TangleLst contract's instance. +TangleLst constant TANGLE_LST_CONTRACT = TangleLst(TANGLE_LST); + +/// @author The Tangle Team +/// @title Pallet TangleLst Interface +/// @title The interface through which solidity contracts will interact with the TangleLst pallet +/// @custom:address 0x0000000000000000000000000000000000000809 +interface TangleLst { + /// @dev Join a pool with a specified amount. + /// @param amount The amount to join with. + /// @param poolId The ID of the pool to join. + function join(uint256 amount, uint256 poolId) external returns (uint8); + + /// @dev Bond extra to a pool. + /// @param poolId The ID of the pool. + /// @param extraType The type of extra bond (0 for FreeBalance, 1 for Rewards). + /// @param extra The amount of extra bond. + function bondExtra(uint256 poolId, uint8 extraType, uint256 extra) external returns (uint8); + + /// @dev Unbond from a pool. + /// @param memberAccount The account of the member. + /// @param poolId The ID of the pool. + /// @param unbondingPoints The amount of unbonding points. + function unbond(bytes32 memberAccount, uint256 poolId, uint256 unbondingPoints) external returns (uint8); + + /// @dev Withdraw unbonded funds from a pool. + /// @param poolId The ID of the pool. + /// @param numSlashingSpans The number of slashing spans. + function poolWithdrawUnbonded(uint256 poolId, uint32 numSlashingSpans) external returns (uint8); + + /// @dev Withdraw unbonded funds for a member. + /// @param memberAccount The account of the member. + /// @param poolId The ID of the pool. + /// @param numSlashingSpans The number of slashing spans. + function withdrawUnbonded(bytes32 memberAccount, uint256 poolId, uint32 numSlashingSpans) external returns (uint8); + + /// @dev Create a new pool. + /// @param amount The initial amount to create the pool with. + /// @param root The root account of the pool. + /// @param nominator The nominator account of the pool. + /// @param bouncer The bouncer account of the pool. + function create(uint256 amount, bytes32 root, bytes32 nominator, bytes32 bouncer) external returns (uint8); + + /// @dev Create a new pool with a specific pool ID. + /// @param amount The initial amount to create the pool with. + /// @param root The root account of the pool. + /// @param nominator The nominator account of the pool. + /// @param bouncer The bouncer account of the pool. + /// @param poolId The desired pool ID. + function createWithPoolId(uint256 amount, bytes32 root, bytes32 nominator, bytes32 bouncer, uint256 poolId) external returns (uint8); + + /// @dev Nominate validators for a pool. + /// @param poolId The ID of the pool. + /// @param validators An array of validator accounts to nominate. + function nominate(uint256 poolId, bytes32[] calldata validators) external returns (uint8); + + /// @dev Set the state of a pool. + /// @param poolId The ID of the pool. + /// @param state The new state (0 for Open, 1 for Blocked, 2 for Destroying). + function setState(uint256 poolId, uint8 state) external returns (uint8); + + /// @dev Set metadata for a pool. + /// @param poolId The ID of the pool. + /// @param metadata The metadata to set. + function setMetadata(uint256 poolId, bytes calldata metadata) external returns (uint8); + + /// @dev Set global configurations (only callable by root). + /// @param minJoinBond The minimum bond required to join a pool (0 for no change). + /// @param minCreateBond The minimum bond required to create a pool (0 for no change). + /// @param maxPools The maximum number of pools (0 for no change). + /// @param globalMaxCommission The global maximum commission percentage (0 for no change). + function setConfigs(uint256 minJoinBond, uint256 minCreateBond, uint32 maxPools, uint32 globalMaxCommission) external returns (uint8); +} \ No newline at end of file diff --git a/precompiles/tangle-lst/src/lib.rs b/precompiles/tangle-lst/src/lib.rs new file mode 100644 index 000000000..13040864e --- /dev/null +++ b/precompiles/tangle-lst/src/lib.rs @@ -0,0 +1,278 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Webb Technologies Inc. +// +// This file is part of pallet-evm-precompile-multi-asset-delegation package. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This file contains the implementation of the MultiAssetDelegationPrecompile struct which provides an +//! interface between the EVM and the native MultiAssetDelegation pallet of the runtime. It allows EVM contracts +//! to call functions of the MultiAssetDelegation pallet, in order to enable EVM accounts to interact with the delegation system. +//! +//! The MultiAssetDelegationPrecompile struct implements core methods that correspond to the functions of the +//! MultiAssetDelegation pallet. These methods can be called from EVM contracts. They include functions to join as an operator, +//! delegate assets, withdraw assets, etc. +//! +//! Each method records the gas cost for the operation, performs the requested operation, and +//! returns the result in a format that can be used by the EVM. +//! +//! The MultiAssetDelegationPrecompile struct is generic over the Runtime type, which is the type of the runtime +//! that includes the MultiAssetDelegation pallet. This allows the precompile to work with any runtime that +//! includes the MultiAssetDelegation pallet and meets the other trait bounds required by the precompile. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +use fp_evm::PrecompileHandle; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + traits::Currency, +}; +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; +use sp_core::{H160, H256, U256}; +use sp_runtime::traits::Dispatchable; +use sp_std::{marker::PhantomData, vec::Vec}; +use tangle_primitives::types::WrappedAccountId32; + +type BalanceOf = + <::Currency as Currency< + ::AccountId, + >>::Balance; + +use pallet_tangle_lst::{PoolId, PoolState, BondExtra}; +use sp_runtime::Perbill; + +pub struct TangleLstPrecompile(PhantomData); + +#[precompile_utils::precompile] +impl TangleLstPrecompile +where + Runtime: pallet_tangle_lst::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + ::RuntimeOrigin: From>, + Runtime::RuntimeCall: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime::AccountId: From, +{ + #[precompile::public("join(uint256,uint256)")] + fn join(handle: &mut impl PrecompileHandle, amount: U256, pool_id: U256) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let amount: BalanceOf = amount.try_into().map_err(|_| revert("Invalid amount"))?; + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + + let call = pallet_tangle_lst::Call::::join { amount, pool_id }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("bondExtra(uint256,uint8,uint256)")] + fn bond_extra(handle: &mut impl PrecompileHandle, pool_id: U256, extra_type: u8, extra: U256) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + let extra: BalanceOf = extra.try_into().map_err(|_| revert("Invalid extra amount"))?; + + let extra = match extra_type { + 0 => BondExtra::FreeBalance(extra), + 1 => BondExtra::Rewards, + _ => return Err(revert("Invalid extra type")), + }; + + let call = pallet_tangle_lst::Call::::bond_extra { pool_id, extra }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("unbond(bytes32,uint256,uint256)")] + fn unbond(handle: &mut impl PrecompileHandle, member_account: H256, pool_id: U256, unbonding_points: U256) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let member_account = Self::convert_to_account_id(member_account)?; + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + let unbonding_points: BalanceOf = unbonding_points.try_into().map_err(|_| revert("Invalid unbonding points"))?; + + let call = pallet_tangle_lst::Call::::unbond { member_account, pool_id, unbonding_points }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("poolWithdrawUnbonded(uint256,uint32)")] + fn pool_withdraw_unbonded(handle: &mut impl PrecompileHandle, pool_id: U256, num_slashing_spans: u32) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + + let call = pallet_tangle_lst::Call::::pool_withdraw_unbonded { pool_id, num_slashing_spans }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("withdrawUnbonded(bytes32,uint256,uint32)")] + fn withdraw_unbonded(handle: &mut impl PrecompileHandle, member_account: H256, pool_id: U256, num_slashing_spans: u32) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let member_account = Self::convert_to_account_id(member_account)?; + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + + let call = pallet_tangle_lst::Call::::withdraw_unbonded { member_account, pool_id, num_slashing_spans }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("create(uint256,bytes32,bytes32,bytes32)")] + fn create(handle: &mut impl PrecompileHandle, amount: U256, root: H256, nominator: H256, bouncer: H256) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let amount: BalanceOf = amount.try_into().map_err(|_| revert("Invalid amount"))?; + let root = Self::convert_to_account_id(root)?; + let nominator = Self::convert_to_account_id(nominator)?; + let bouncer = Self::convert_to_account_id(bouncer)?; + + let call = pallet_tangle_lst::Call::::create { amount, root, nominator, bouncer }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("createWithPoolId(uint256,bytes32,bytes32,bytes32,uint256)")] + fn create_with_pool_id(handle: &mut impl PrecompileHandle, amount: U256, root: H256, nominator: H256, bouncer: H256, pool_id: U256) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let amount: BalanceOf = amount.try_into().map_err(|_| revert("Invalid amount"))?; + let root = Self::convert_to_account_id(root)?; + let nominator = Self::convert_to_account_id(nominator)?; + let bouncer = Self::convert_to_account_id(bouncer)?; + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + + let call = pallet_tangle_lst::Call::::create_with_pool_id { amount, root, nominator, bouncer, pool_id }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("nominate(uint256,bytes32[])")] + fn nominate(handle: &mut impl PrecompileHandle, pool_id: U256, validators: Vec) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + let validators: Vec = validators.into_iter().map(Self::convert_to_account_id).collect::>()?; + + let call = pallet_tangle_lst::Call::::nominate { pool_id, validators }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("setState(uint256,uint8)")] + fn set_state(handle: &mut impl PrecompileHandle, pool_id: U256, state: u8) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + let state = match state { + 0 => PoolState::Open, + 1 => PoolState::Blocked, + 2 => PoolState::Destroying, + _ => return Err(revert("Invalid state")), + }; + + let call = pallet_tangle_lst::Call::::set_state { pool_id, state }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("setMetadata(uint256,bytes)")] + fn set_metadata(handle: &mut impl PrecompileHandle, pool_id: U256, metadata: Vec) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let pool_id: PoolId = pool_id.try_into().map_err(|_| revert("Invalid pool id"))?; + + let call = pallet_tangle_lst::Call::::set_metadata { pool_id, metadata }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("setConfigs(uint256,uint256,uint32,uint32)")] + fn set_configs( + handle: &mut impl PrecompileHandle, + min_join_bond: U256, + min_create_bond: U256, + max_pools: u32, + global_max_commission: u32, + ) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + ensure_root(handle)?; + + let min_join_bond: Option> = if min_join_bond == U256::zero() { + None + } else { + Some(min_join_bond.try_into().map_err(|_| revert("Invalid min join bond"))?) + }; + + let min_create_bond: Option> = if min_create_bond == U256::zero() { + None + } else { + Some(min_create_bond.try_into().map_err(|_| revert("Invalid min create bond"))?) + }; + + let max_pools = if max_pools == 0 { None } else { Some(max_pools) }; + + let global_max_commission = if global_max_commission == 0 { + None + } else { + Some(Perbill::from_percent(global_max_commission)) + }; + + let call = pallet_tangle_lst::Call::::set_configs { + min_join_bond: min_join_bond.map(ConfigOp::Set).unwrap_or(ConfigOp::Noop), + min_create_bond: min_create_bond.map(ConfigOp::Set).unwrap_or(ConfigOp::Noop), + max_pools: max_pools.map(ConfigOp::Set).unwrap_or(ConfigOp::Noop), + global_max_commission: global_max_commission.map(ConfigOp::Set).unwrap_or(ConfigOp::Noop), + }; + + RuntimeHelper::::try_dispatch(handle, RawOrigin::Root.into(), call)?; + + Ok(()) + } +} diff --git a/precompiles/tangle-lst/src/mock.rs b/precompiles/tangle-lst/src/mock.rs new file mode 100644 index 000000000..71bad6731 --- /dev/null +++ b/precompiles/tangle-lst/src/mock.rs @@ -0,0 +1,391 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Webb Technologies Inc. +// +// This file is part of pallet-evm-precompile-multi-asset-delegation package. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities +use super::*; +use crate::{MultiAssetDelegationPrecompile, MultiAssetDelegationPrecompileCall}; +use frame_support::traits::AsEnsureOriginWithArg; +use frame_support::PalletId; +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU64, Everything}, + weights::Weight, +}; +use pallet_evm::{EnsureAddressNever, EnsureAddressOrigin, SubstrateBlockHashMapping}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use precompile_utils::precompile_set::{AddressU64, PrecompileAt, PrecompileSetBuilder}; +use tangle_primitives::ServiceManager; + +use serde::{Deserialize, Serialize}; +use sp_core::{ + self, + sr25519::{Public as sr25519Public, Signature}, + ConstU32, H160, H256, U256, +}; +use sp_runtime::{ + traits::{IdentifyAccount, IdentityLookup, Verify}, + AccountId32, BuildStorage, +}; + +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Balance = u64; + +type Block = frame_system::mocking::MockBlock; +type AssetId = u32; + +const PRECOMPILE_ADDRESS_BYTES: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, +]; + +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Clone, + Encode, + Decode, + Debug, + MaxEncodedLen, + Serialize, + Deserialize, + derive_more::Display, + scale_info::TypeInfo, +)] +pub enum TestAccount { + Empty, + Alex, + Bobo, + Dave, + Charlie, + Eve, + PrecompileAddress, +} + +impl Default for TestAccount { + fn default() -> Self { + Self::Empty + } +} + +// needed for associated type in pallet_evm +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> AccountId32 { + match h160_account { + a if a == H160::repeat_byte(0x01) => TestAccount::Alex.into(), + a if a == H160::repeat_byte(0x02) => TestAccount::Bobo.into(), + a if a == H160::repeat_byte(0x03) => TestAccount::Charlie.into(), + a if a == H160::repeat_byte(0x04) => TestAccount::Dave.into(), + a if a == H160::repeat_byte(0x05) => TestAccount::Eve.into(), + a if a == H160::from_low_u64_be(6) => TestAccount::PrecompileAddress.into(), + _ => TestAccount::Empty.into(), + } + } +} + +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> sp_core::sr25519::Public { + match h160_account { + a if a == H160::repeat_byte(0x01) => sr25519Public::from_raw([1u8; 32]), + a if a == H160::repeat_byte(0x02) => sr25519Public::from_raw([2u8; 32]), + a if a == H160::repeat_byte(0x03) => sr25519Public::from_raw([3u8; 32]), + a if a == H160::repeat_byte(0x04) => sr25519Public::from_raw([4u8; 32]), + a if a == H160::repeat_byte(0x05) => sr25519Public::from_raw([5u8; 32]), + a if a == H160::from_low_u64_be(6) => sr25519Public::from_raw(PRECOMPILE_ADDRESS_BYTES), + _ => sr25519Public::from_raw([0u8; 32]), + } + } +} + +impl From for H160 { + fn from(x: TestAccount) -> H160 { + match x { + TestAccount::Alex => H160::repeat_byte(0x01), + TestAccount::Bobo => H160::repeat_byte(0x02), + TestAccount::Charlie => H160::repeat_byte(0x03), + TestAccount::Dave => H160::repeat_byte(0x04), + TestAccount::Eve => H160::repeat_byte(0x05), + TestAccount::PrecompileAddress => H160::from_low_u64_be(6), + _ => Default::default(), + } + } +} + +impl From for AccountId32 { + fn from(x: TestAccount) -> Self { + match x { + TestAccount::Alex => AccountId32::from([1u8; 32]), + TestAccount::Bobo => AccountId32::from([2u8; 32]), + TestAccount::Charlie => AccountId32::from([3u8; 32]), + TestAccount::Dave => AccountId32::from([4u8; 32]), + TestAccount::Eve => AccountId32::from([5u8; 32]), + TestAccount::PrecompileAddress => AccountId32::from(PRECOMPILE_ADDRESS_BYTES), + _ => AccountId32::from([0u8; 32]), + } + } +} + +impl From for sp_core::sr25519::Public { + fn from(x: TestAccount) -> Self { + match x { + TestAccount::Alex => sr25519Public::from_raw([1u8; 32]), + TestAccount::Bobo => sr25519Public::from_raw([2u8; 32]), + TestAccount::Charlie => sr25519Public::from_raw([3u8; 32]), + TestAccount::Dave => sr25519Public::from_raw([4u8; 32]), + TestAccount::Eve => sr25519Public::from_raw([5u8; 32]), + TestAccount::PrecompileAddress => sr25519Public::from_raw(PRECOMPILE_ADDRESS_BYTES), + _ => sr25519Public::from_raw([0u8; 32]), + } + } +} + +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + Assets: pallet_assets, + MultiAssetDelegation: pallet_multi_asset_delegation, + } +); + +parameter_types! { + pub const SS58Prefix: u8 = 42; + pub static ExistentialDeposit: Balance = 1; +} +impl frame_system::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Block = Block; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type RuntimeTask = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +pub type Precompiles = + PrecompileSetBuilder, MultiAssetDelegationPrecompile>,)>; + +pub type PCall = MultiAssetDelegationPrecompileCall; + +pub struct EnsureAddressAlways; +impl EnsureAddressOrigin for EnsureAddressAlways { + type Success = (); + + fn try_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } + + fn ensure_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } +} + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; + pub SuicideQuickClearLimit: u32 = 0; + +} +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressAlways; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = TestAccount; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type SuicideQuickClearLimit = SuicideQuickClearLimit; + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = AssetId; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU64<1>; + type AssetAccountDeposit = ConstU64<10>; + type MetadataDepositBase = ConstU64<1>; + type MetadataDepositPerByte = ConstU64<1>; + type ApprovalDeposit = ConstU64<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = (); + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; +} + +pub struct MockServiceManager; + +impl ServiceManager for MockServiceManager { + fn get_active_blueprints_count(_account: &AccountId) -> usize { + // we dont care + Default::default() + } + + fn get_active_services_count(_account: &AccountId) -> usize { + // we dont care + Default::default() + } + + fn can_exit(_account: &AccountId) -> bool { + // Mock logic to determine if the given account can exit + true + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaxLocks: u32 = 50; + pub const MinOperatorBondAmount: u64 = 10_000; + pub const BondDuration: u32 = 10; + pub PID: PalletId = PalletId(*b"PotStake"); +} + +impl pallet_multi_asset_delegation::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type MinOperatorBondAmount = MinOperatorBondAmount; + type BondDuration = BondDuration; + type ServiceManager = MockServiceManager; + type LeaveOperatorsDelay = ConstU32<10>; + type OperatorBondLessDelay = ConstU32<1>; + type LeaveDelegatorsDelay = ConstU32<1>; + type DelegationBondLessDelay = ConstU32<5>; + type MinDelegateAmount = ConstU64<100>; + type Fungibles = Assets; + type AssetId = AssetId; + type PoolId = AssetId; + type ForceOrigin = frame_system::EnsureRoot; + type PalletId = PID; + type WeightInfo = (); +} + +/// Build test externalities, prepopulated with data for testing democracy precompiles +#[derive(Default)] +pub(crate) struct ExtBuilder { + /// Endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl ExtBuilder { + /// Build the test externalities for use in tests + 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"); + + pallet_balances::GenesisConfig:: { + balances: self + .balances + .iter() + .chain( + [ + (TestAccount::Alex.into(), 1_000_000), + (TestAccount::Bobo.into(), 1_000_000), + (TestAccount::Charlie.into(), 1_000_000), + (MultiAssetDelegation::pallet_account(), 100), // give pallet some ED so it can receive tokens + ] + .iter(), + ) + .cloned() + .collect(), + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext + } +} diff --git a/precompiles/tangle-lst/src/tests.rs b/precompiles/tangle-lst/src/tests.rs new file mode 100644 index 000000000..b42eef7ed --- /dev/null +++ b/precompiles/tangle-lst/src/tests.rs @@ -0,0 +1,444 @@ +use crate::mock::*; +use crate::U256; +use frame_support::assert_ok; +use frame_support::traits::Currency; +use pallet_multi_asset_delegation::{types::OperatorStatus, CurrentRound, Delegators, Operators}; +use precompile_utils::testing::*; +use sp_core::H160; + +// Helper function for creating and minting tokens +pub fn create_and_mint_tokens( + asset_id: u32, + recipient: ::AccountId, + amount: Balance, +) { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, recipient, false, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(recipient), asset_id, recipient, amount)); +} + +#[test] +fn test_selector_less_than_four_bytes_reverts() { + ExtBuilder::default().build().execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, vec![1u8, 2, 3]) + .execute_reverts(|output| output == b"Tried to read selector out of bounds"); + }); +} + +#[test] +fn test_unimplemented_selector_reverts() { + ExtBuilder::default().build().execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, vec![1u8, 2, 3, 4]) + .execute_reverts(|output| output == b"Unknown selector"); + }); +} + +#[test] +fn test_join_operators() { + ExtBuilder::default().build().execute_with(|| { + let account = sp_core::sr25519::Public::from(TestAccount::Alex); + let initial_balance = Balances::free_balance(account); + assert!(Operators::::get(account).is_none()); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::join_operators { bond_amount: U256::from(10_000) }, + ) + .execute_returns(()); + + assert!(Operators::::get(account).is_some()); + let expected_balance = initial_balance - 10_000; + assert_eq!(Balances::free_balance(account), expected_balance); + }); +} + +#[test] +fn test_join_operators_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + let account = sp_core::sr25519::Public::from(TestAccount::Eve); + Balances::make_free_balance_be(&account, 500); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Eve, + H160::from_low_u64_be(1), + PCall::join_operators { bond_amount: U256::from(10_000) }, + ) + .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 1, error: [2, 0, 0, 0], message: Some(\"InsufficientBalance\") })"); + + assert_eq!(Balances::free_balance(account), 500); + }); +} + +#[test] +fn test_delegate_assets_invalid_operator() { + ExtBuilder::default().build().execute_with(|| { + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Alex); + + Balances::make_free_balance_be(&delegator_account, 500); + create_and_mint_tokens(1, delegator_account, 500); + + assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), 1, 200)); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::delegate { + operator: sp_core::sr25519::Public::from(TestAccount::Eve).into(), + asset_id: U256::from(1), + amount: U256::from(100), + }, + ) + .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 5, error: [2, 0, 0, 0], message: Some(\"NotAnOperator\") })"); + + assert_eq!(Balances::free_balance(delegator_account), 500); + }); +} + +#[test] +fn test_delegate_assets() { + ExtBuilder::default().build().execute_with(|| { + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Alex); + + Balances::make_free_balance_be(&operator_account, 20_000); + Balances::make_free_balance_be(&delegator_account, 500); + + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + create_and_mint_tokens(1, delegator_account, 500); + assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), 1, 200)); + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // should lose deposit + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::delegate { + operator: operator_account.into(), + asset_id: U256::from(1), + amount: U256::from(100), + }, + ) + .execute_returns(()); + + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // no change when delegating + }); +} + +#[test] +fn test_delegate_assets_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Eve); + + Balances::make_free_balance_be(&operator_account, 20_000); + Balances::make_free_balance_be(&delegator_account, 500); + + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + create_and_mint_tokens(1, delegator_account, 500); + + assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), 1, 200)); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Eve, + H160::from_low_u64_be(1), + PCall::delegate { + operator: operator_account.into(), + asset_id: U256::from(1), + amount: U256::from(300), + }, + ) + .execute_reverts(|output| output == b"Dispatched call failed with error: Module(ModuleError { index: 5, error: [14, 0, 0, 0], message: Some(\"InsufficientBalance\") })"); + + assert_eq!(Balances::free_balance(delegator_account), 500); + }); +} + +#[test] +fn test_schedule_withdraw() { + ExtBuilder::default().build().execute_with(|| { + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Alex); + + Balances::make_free_balance_be(&operator_account, 20_000); + Balances::make_free_balance_be(&delegator_account, 500); + + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + create_and_mint_tokens(1, delegator_account, 500); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::deposit { asset_id: U256::from(1), amount: U256::from(200) }, + ) + .execute_returns(()); + + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // should lose deposit + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::delegate { + operator: operator_account.into(), + asset_id: U256::from(1), + amount: U256::from(100), + }, + ) + .execute_returns(()); + + assert!(Delegators::::get(delegator_account).is_some()); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::schedule_withdraw { asset_id: U256::from(1), amount: U256::from(100) }, + ) + .execute_returns(()); + + let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); + assert_eq!(metadata.deposits.get(&1), None); + assert!(!metadata.withdraw_requests.is_empty()); + + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // no change + }); +} + +#[test] +fn test_execute_withdraw() { + ExtBuilder::default().build().execute_with(|| { + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Alex); + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + + Balances::make_free_balance_be(&operator_account, 20_000); + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + create_and_mint_tokens(1, delegator_account, 500); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::deposit { asset_id: U256::from(1), amount: U256::from(200) }, + ) + .execute_returns(()); + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // should lose deposit + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::delegate { + operator: operator_account.into(), + asset_id: U256::from(1), + amount: U256::from(100), + }, + ) + .execute_returns(()); + + assert!(Delegators::::get(delegator_account).is_some()); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::schedule_withdraw { asset_id: U256::from(1), amount: U256::from(100) }, + ) + .execute_returns(()); + + let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); + assert_eq!(metadata.deposits.get(&1), None); + assert!(!metadata.withdraw_requests.is_empty()); + + >::put(3); + + PrecompilesValue::get() + .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::execute_withdraw {}) + .execute_returns(()); + + let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); + assert_eq!(metadata.deposits.get(&1), None); + assert!(metadata.withdraw_requests.is_empty()); + + assert_eq!(Assets::balance(1, delegator_account), 500 - 100); // deposited 200, withdrew 100 + }); +} + +#[test] +fn test_execute_withdraw_before_due() { + ExtBuilder::default().build().execute_with(|| { + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Alex); + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + + Balances::make_free_balance_be(&delegator_account, 10_000); + Balances::make_free_balance_be(&operator_account, 20_000); + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + create_and_mint_tokens(1, delegator_account, 500); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::deposit { asset_id: U256::from(1), amount: U256::from(200) }, + ) + .execute_returns(()); + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // should lose deposit + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::delegate { + operator: operator_account.into(), + asset_id: U256::from(1), + amount: U256::from(100), + }, + ) + .execute_returns(()); + + assert!(Delegators::::get(delegator_account).is_some()); + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // delegate should not change balance + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::schedule_withdraw { asset_id: U256::from(1), amount: U256::from(100) }, + ) + .execute_returns(()); + + let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); + assert_eq!(metadata.deposits.get(&1), None); + assert!(!metadata.withdraw_requests.is_empty()); + + PrecompilesValue::get() + .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::execute_withdraw {}) + .execute_returns(()); // should not fail + + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // not expired so should not change balance + }); +} + +#[test] +fn test_cancel_withdraw() { + ExtBuilder::default().build().execute_with(|| { + let delegator_account = sp_core::sr25519::Public::from(TestAccount::Alex); + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + + Balances::make_free_balance_be(&operator_account, 20_000); + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + create_and_mint_tokens(1, delegator_account, 500); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::deposit { asset_id: U256::from(1), amount: U256::from(200) }, + ) + .execute_returns(()); + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // should lose deposit + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::delegate { + operator: operator_account.into(), + asset_id: U256::from(1), + amount: U256::from(100), + }, + ) + .execute_returns(()); + + assert!(Delegators::::get(delegator_account).is_some()); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::schedule_withdraw { asset_id: U256::from(1), amount: U256::from(100) }, + ) + .execute_returns(()); + + let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); + assert_eq!(metadata.deposits.get(&1), None); + assert!(!metadata.withdraw_requests.is_empty()); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::cancel_withdraw { asset_id: U256::from(1), amount: U256::from(100) }, + ) + .execute_returns(()); + + let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); + assert!(metadata.deposits.contains_key(&1)); + assert!(metadata.withdraw_requests.is_empty()); + + assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // no change + }); +} + +#[test] +fn test_operator_go_offline_and_online() { + ExtBuilder::default().build().execute_with(|| { + let operator_account = sp_core::sr25519::Public::from(TestAccount::Bobo); + + Balances::make_free_balance_be(&operator_account, 20_000); + assert_ok!(MultiAssetDelegation::join_operators( + RuntimeOrigin::signed(operator_account), + 10_000 + )); + + PrecompilesValue::get() + .prepare_test(TestAccount::Bobo, H160::from_low_u64_be(1), PCall::go_offline {}) + .execute_returns(()); + + assert!( + MultiAssetDelegation::operator_info(operator_account).unwrap().status + == OperatorStatus::Inactive + ); + + PrecompilesValue::get() + .prepare_test(TestAccount::Bobo, H160::from_low_u64_be(1), PCall::go_online {}) + .execute_returns(()); + + assert!( + MultiAssetDelegation::operator_info(operator_account).unwrap().status + == OperatorStatus::Active + ); + + assert_eq!(Balances::free_balance(operator_account), 20_000 - 10_000); + }); +} diff --git a/runtime/mainnet/Cargo.toml b/runtime/mainnet/Cargo.toml index 8df55ee14..8fb50b82f 100644 --- a/runtime/mainnet/Cargo.toml +++ b/runtime/mainnet/Cargo.toml @@ -81,6 +81,7 @@ pallet-transaction-payment-rpc-runtime-api = { workspace = true } pallet-utility = { workspace = true } pallet-multisig = { workspace = true } pallet-vesting = { workspace = true } +pallet-tangle-lst = { workspace = true } # Tangle dependencies pallet-airdrop-claims = { workspace = true } @@ -214,6 +215,7 @@ std = [ "pallet-identity/std", "frame-system-benchmarking?/std", "sp-storage/std", + "pallet-tangle-lst/std", "pallet-assets/std", # Tangle dependencies diff --git a/runtime/mainnet/src/lib.rs b/runtime/mainnet/src/lib.rs index cce401fee..917bbb92e 100644 --- a/runtime/mainnet/src/lib.rs +++ b/runtime/mainnet/src/lib.rs @@ -1265,6 +1265,34 @@ impl pallet_multi_asset_delegation::Config for Runtime { type WeightInfo = (); } +parameter_types! { + pub static PostUnbondingPoolsWindow: u32 = 2; + pub static MaxMetadataLen: u32 = 2; + pub static CheckLevel: u8 = 255; + pub const LstPalletId: PalletId = PalletId(*b"py/tnlst"); +} + +impl pallet_tangle_lst::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RewardCounter = FixedU128; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = Staking; + type PostUnbondingPoolsWindow = ConstU32<4>; + type PalletId = LstPalletId; + type MaxMetadataLen = MaxMetadataLen; + // we use the same number of allowed unlocking chunks as with staking. + type MaxUnbonding = ::MaxUnlockingChunks; + type Fungibles = Assets; + type AssetId = AssetId; + type PoolId = AssetId; + type ForceOrigin = frame_system::EnsureRoot; + type MaxPointsToBalance = frame_support::traits::ConstU8<10>; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime { @@ -1316,6 +1344,7 @@ construct_runtime!( HotfixSufficients: pallet_hotfix_sufficients = 38, Claims: pallet_airdrop_claims = 39, + Lst: pallet_tangle_lst = 40, // DO NOT USE below indexes // Roles: pallet_roles = 40, // Jobs: pallet_jobs = 41, diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index 08f7f3b1f..b7ac06c55 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -95,6 +95,7 @@ pallet-services-rpc-runtime-api = { workspace = true } tangle-primitives = { workspace = true, features = ["verifying"] } tangle-crypto-primitives = { workspace = true } pallet-multi-asset-delegation = { workspace = true } +pallet-tangle-lst = { workspace = true } # Frontier dependencies fp-evm = { workspace = true } @@ -255,6 +256,7 @@ std = [ "tangle-crypto-primitives/std", "pallet-services/std", "pallet-multi-asset-delegation/std", + "pallet-tangle-lst/std", "pallet-services-rpc-runtime-api/std", # Frontier diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index 67b28ee0f..1913ef466 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -1219,6 +1219,34 @@ impl pallet_proxy::Config for Runtime { type AnnouncementDepositFactor = AnnouncementDepositFactor; } +parameter_types! { + pub static PostUnbondingPoolsWindow: u32 = 2; + pub static MaxMetadataLen: u32 = 2; + pub static CheckLevel: u8 = 255; + pub const LstPalletId: PalletId = PalletId(*b"py/tnlst"); +} + +impl pallet_tangle_lst::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RewardCounter = FixedU128; + type BalanceToU256 = BalanceToU256; + type U256ToBalance = U256ToBalance; + type Staking = Staking; + type PostUnbondingPoolsWindow = ConstU32<4>; + type PalletId = LstPalletId; + type MaxMetadataLen = MaxMetadataLen; + // we use the same number of allowed unlocking chunks as with staking. + type MaxUnbonding = ::MaxUnlockingChunks; + type Fungibles = Assets; + type AssetId = AssetId; + type PoolId = AssetId; + type ForceOrigin = frame_system::EnsureRoot; + type MaxPointsToBalance = frame_support::traits::ConstU8<10>; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime { @@ -1280,6 +1308,7 @@ construct_runtime!( Proxy: pallet_proxy = 44, MultiAssetDelegation: pallet_multi_asset_delegation = 45, Services: pallet_services = 51, + Lst: pallet_tangle_lst = 52, // Sygma SygmaAccessSegregator: sygma_access_segregator = 46, diff --git a/tangle-subxt/src/tangle_mainnet_runtime.rs b/tangle-subxt/src/tangle_mainnet_runtime.rs index f73cce3d1..4892c937c 100644 --- a/tangle-subxt/src/tangle_mainnet_runtime.rs +++ b/tangle-subxt/src/tangle_mainnet_runtime.rs @@ -8754,13 +8754,13 @@ pub mod api { #[encode_as_type( crate_path = ":: subxt :: ext :: subxt_core :: ext :: scale_encode" )] - #[doc = "See [`Pallet::force_set_balance`]."] + #[doc = "See [`Pallet::force_make_free_balance_be`]."] pub struct ForceSetBalance { - pub who: force_set_balance::Who, + pub who: force_make_free_balance_be::Who, #[codec(compact)] - pub new_free: force_set_balance::NewFree, + pub new_free: force_make_free_balance_be::NewFree, } - pub mod force_set_balance { + pub mod force_make_free_balance_be { use super::runtime_types; pub type Who = ::subxt::ext::subxt_core::utils::MultiAddress< ::subxt::ext::subxt_core::utils::AccountId32, @@ -8770,7 +8770,7 @@ pub mod api { } impl ::subxt::ext::subxt_core::blocks::StaticExtrinsic for ForceSetBalance { const PALLET: &'static str = "Balances"; - const CALL: &'static str = "force_set_balance"; + const CALL: &'static str = "force_make_free_balance_be"; } #[derive( :: subxt :: ext :: subxt_core :: ext :: codec :: Decode, @@ -8914,15 +8914,15 @@ pub mod api { ], ) } - #[doc = "See [`Pallet::force_set_balance`]."] - pub fn force_set_balance( + #[doc = "See [`Pallet::force_make_free_balance_be`]."] + pub fn force_make_free_balance_be( &self, - who: types::force_set_balance::Who, - new_free: types::force_set_balance::NewFree, + who: types::force_make_free_balance_be::Who, + new_free: types::force_make_free_balance_be::NewFree, ) -> ::subxt::ext::subxt_core::tx::payload::StaticPayload { ::subxt::ext::subxt_core::tx::payload::StaticPayload::new_static( "Balances", - "force_set_balance", + "force_make_free_balance_be", types::ForceSetBalance { who, new_free }, [ 101u8, 181u8, 86u8, 32u8, 61u8, 75u8, 34u8, 164u8, 142u8, 250u8, 7u8, @@ -27742,7 +27742,7 @@ pub mod api { ], ) } - #[doc = " Maximum number of nomination pools that can exist. If `None`, then an unbounded number of"] + #[doc = " Maximum number of tangle-lst that can exist. If `None`, then an unbounded number of"] #[doc = " pools can exist."] pub fn max_pools( &self, @@ -39811,8 +39811,8 @@ pub mod api { >, }, #[codec(index = 8)] - #[doc = "See [`Pallet::force_set_balance`]."] - force_set_balance { + #[doc = "See [`Pallet::force_make_free_balance_be`]."] + force_make_free_balance_be { who: ::subxt::ext::subxt_core::utils::MultiAddress< ::subxt::ext::subxt_core::utils::AccountId32, ::core::primitive::u32, diff --git a/tangle-subxt/src/tangle_testnet_runtime.rs b/tangle-subxt/src/tangle_testnet_runtime.rs index ee340fcb8..bae5873a1 100644 --- a/tangle-subxt/src/tangle_testnet_runtime.rs +++ b/tangle-subxt/src/tangle_testnet_runtime.rs @@ -8558,13 +8558,13 @@ pub mod api { #[encode_as_type( crate_path = ":: subxt :: ext :: subxt_core :: ext :: scale_encode" )] - #[doc = "See [`Pallet::force_set_balance`]."] + #[doc = "See [`Pallet::force_make_free_balance_be`]."] pub struct ForceSetBalance { - pub who: force_set_balance::Who, + pub who: force_make_free_balance_be::Who, #[codec(compact)] - pub new_free: force_set_balance::NewFree, + pub new_free: force_make_free_balance_be::NewFree, } - pub mod force_set_balance { + pub mod force_make_free_balance_be { use super::runtime_types; pub type Who = ::subxt::ext::subxt_core::utils::MultiAddress< ::subxt::ext::subxt_core::utils::AccountId32, @@ -8574,7 +8574,7 @@ pub mod api { } impl ::subxt::ext::subxt_core::blocks::StaticExtrinsic for ForceSetBalance { const PALLET: &'static str = "Balances"; - const CALL: &'static str = "force_set_balance"; + const CALL: &'static str = "force_make_free_balance_be"; } #[derive( :: subxt :: ext :: subxt_core :: ext :: codec :: Decode, @@ -8718,15 +8718,15 @@ pub mod api { ], ) } - #[doc = "See [`Pallet::force_set_balance`]."] - pub fn force_set_balance( + #[doc = "See [`Pallet::force_make_free_balance_be`]."] + pub fn force_make_free_balance_be( &self, - who: types::force_set_balance::Who, - new_free: types::force_set_balance::NewFree, + who: types::force_make_free_balance_be::Who, + new_free: types::force_make_free_balance_be::NewFree, ) -> ::subxt::ext::subxt_core::tx::payload::StaticPayload { ::subxt::ext::subxt_core::tx::payload::StaticPayload::new_static( "Balances", - "force_set_balance", + "force_make_free_balance_be", types::ForceSetBalance { who, new_free }, [ 101u8, 181u8, 86u8, 32u8, 61u8, 75u8, 34u8, 164u8, 142u8, 250u8, 7u8, @@ -27545,7 +27545,7 @@ pub mod api { ], ) } - #[doc = " Maximum number of nomination pools that can exist. If `None`, then an unbounded number of"] + #[doc = " Maximum number of tangle-lst that can exist. If `None`, then an unbounded number of"] #[doc = " pools can exist."] pub fn max_pools( &self, @@ -45952,8 +45952,8 @@ pub mod api { >, }, #[codec(index = 8)] - #[doc = "See [`Pallet::force_set_balance`]."] - force_set_balance { + #[doc = "See [`Pallet::force_make_free_balance_be`]."] + force_make_free_balance_be { who: ::subxt::ext::subxt_core::utils::MultiAddress< ::subxt::ext::subxt_core::utils::AccountId32, ::core::primitive::u32, diff --git a/types/src/interfaces/augment-api-query.ts b/types/src/interfaces/augment-api-query.ts index 8d4acda79..e0206ea89 100644 --- a/types/src/interfaces/augment-api-query.ts +++ b/types/src/interfaces/augment-api-query.ts @@ -853,7 +853,7 @@ declare module '@polkadot/api-base/types/storage' { **/ maxPoolMembersPerPool: AugmentedQuery Observable>, []> & QueryableStorageEntry; /** - * Maximum number of nomination pools that can exist. If `None`, then an unbounded number of + * Maximum number of tangle-lst that can exist. If `None`, then an unbounded number of * pools can exist. **/ maxPools: AugmentedQuery Observable>, []> & QueryableStorageEntry; diff --git a/types/src/interfaces/augment-api-tx.ts b/types/src/interfaces/augment-api-tx.ts index ead60f8f8..df1bef37b 100644 --- a/types/src/interfaces/augment-api-tx.ts +++ b/types/src/interfaces/augment-api-tx.ts @@ -194,7 +194,7 @@ declare module '@polkadot/api-base/types/submittable' { **/ forceAdjustTotalIssuance: AugmentedSubmittable<(direction: PalletBalancesAdjustmentDirection | 'Increase' | 'Decrease' | number | Uint8Array, delta: Compact | AnyNumber | Uint8Array) => SubmittableExtrinsic, [PalletBalancesAdjustmentDirection, Compact]>; /** - * See [`Pallet::force_set_balance`]. + * See [`Pallet::force_make_free_balance_be`]. **/ forceSetBalance: AugmentedSubmittable<(who: MultiAddress | { Id: any } | { Index: any } | { Raw: any } | { Address32: any } | { Address20: any } | string | Uint8Array, newFree: Compact | AnyNumber | Uint8Array) => SubmittableExtrinsic, [MultiAddress, Compact]>; /** diff --git a/types/src/interfaces/lookup.ts b/types/src/interfaces/lookup.ts index f25e1884f..2682f1e2d 100644 --- a/types/src/interfaces/lookup.ts +++ b/types/src/interfaces/lookup.ts @@ -2403,7 +2403,7 @@ export default { who: 'Vec', }, __Unused7: 'Null', - force_set_balance: { + force_make_free_balance_be: { who: 'MultiAddress', newFree: 'Compact', },