From 7aaea58da9f355fcd9f665c42f9c59a2cce0c90e Mon Sep 17 00:00:00 2001 From: shannonwells Date: Tue, 30 May 2023 18:07:22 -0700 Subject: [PATCH 01/11] starting prototyping --- common/primitives/src/capacity.rs | 34 +++++++++++++++++++++++++++++++ pallets/capacity/src/lib.rs | 12 +++++++---- runtime/frequency/src/lib.rs | 3 ++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/common/primitives/src/capacity.rs b/common/primitives/src/capacity.rs index d361981d7c..66a7dbf931 100644 --- a/common/primitives/src/capacity.rs +++ b/common/primitives/src/capacity.rs @@ -4,6 +4,8 @@ use frame_support::traits::tokens::Balance; use scale_info::TypeInfo; use sp_api::Decode; use sp_runtime::DispatchError; +use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay}; +use crate::node::{AccountId, Era, Hash}; /// A trait for checking that a target MSA can be staked to. pub trait TargetValidator { @@ -65,3 +67,35 @@ pub enum StakingType { /// and token for the account holder ProviderBoost, } + + +pub trait StakingRewardsProvider { + type Balance: Balance; + /// Return the size of the reward pool for the given era, in token + /// Errors: + /// - EraOutOfRange when `era` is prior to the history retention limit, or greater than the current Era. + fn reward_pool_size(era: Era) -> Self::Balance; + + /// Return the total unclaimed reward in token for `accountId` for `fromEra` --> `toEra`, inclusive + /// Errors: + /// - NotAStakingAccount + /// - EraOutOfRange when fromEra or toEra are prior to the history retention limit, or greater than the current Era. + fn staking_reward_total(accountId: AccountId, fromEra: Era, toEra: Era); + + /// Validate a payout claim for `accountId`, using `proof` and the provided `payload` StakingRewardClaim. + /// Returns whether the claim passes validation. Accounts must first pass `payoutEligible` test. + /// Errors: + /// - NotAStakingAccount + /// - MaxUnlockingChunksExceeded + /// - All other conditions that would prevent a reward from being claimed return 'false' + fn validate_staking_reward_claim(accountId: AccountId, proof: Hash, payload: StakingRewardClaim) -> bool; + + /// Return whether `accountId` can claim a reward. Staking accounts may not claim a reward more than once + /// per Era, may not claim rewards before a complete Era has been staked, and may not claim more rewards past + /// the number of `MaxUnlockingChunks`. + /// Errors: + /// - NotAStakingAccount + /// - MaxUnlockingChunksExceeded + /// - All other conditions that would prevent a reward from being claimed return 'false' + fn payout_eligible(accountId: AccountIdOf) -> bool; +} diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index 7291ceabc5..68ecda5dd0 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -70,12 +70,10 @@ pub use common_primitives::{ #[cfg(feature = "runtime-benchmarks")] use common_primitives::benchmarks::RegisterProviderBenchmarkHelper; -use common_primitives::capacity::StakingType; pub use pallet::*; pub use types::*; pub use weights::*; - pub mod types; #[cfg(feature = "runtime-benchmarks")] @@ -90,12 +88,12 @@ type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; const STAKING_ID: LockIdentifier = *b"netstkng"; - #[frame_support::pallet] pub mod pallet { use super::*; - use codec::EncodeLike; + use codec::EncodeLike; + use common_primitives::capacity::StakingRewardsProvider; use frame_support::{pallet_prelude::*, Twox64Concat}; use frame_system::pallet_prelude::*; use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay}; @@ -763,6 +761,12 @@ impl Pallet { } } + pub fn payout_eligible(account_id: T::AccountId) -> boolean { + let _staking_account = + Self::get_staking_account_for(unstaker).ok_or(Error::::StakingAccountNotFound)?; + false + } + fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight { let current_era_info: RewardEraInfo = Self::get_current_era(); if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index baee758495..6ba10c4aaf 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -78,6 +78,7 @@ pub use common_runtime::{ }; use frame_support::traits::Contains; +use common_primitives::capacity::StakingRewardsProvider; #[cfg(feature = "try-runtime")] use frame_support::traits::TryStateSelect; @@ -739,7 +740,7 @@ use pallet_frequency_tx_payment::Call as FrequencyPaymentCall; use pallet_handles::Call as HandlesCall; use pallet_messages::Call as MessagesCall; use pallet_msa::Call as MsaCall; -use pallet_stateful_storage::Call as StatefulStorageCall; +use pallet_stateful_storage::{types::ItemAction::Delete, Call as StatefulStorageCall}; pub struct CapacityEligibleCalls; impl GetStableWeight for CapacityEligibleCalls { From b8aff883e4c48d938f1a4dde9ae4197845f01ec0 Mon Sep 17 00:00:00 2001 From: shannonwells Date: Thu, 1 Jun 2023 16:27:58 -0700 Subject: [PATCH 02/11] make check working WIP --- pallets/capacity/src/lib.rs | 12 +++++++++--- pallets/capacity/src/types.rs | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index 68ecda5dd0..583dafe2c9 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -70,6 +70,10 @@ pub use common_primitives::{ #[cfg(feature = "runtime-benchmarks")] use common_primitives::benchmarks::RegisterProviderBenchmarkHelper; +use common_primitives::{ + capacity::StakingType, + node::{RewardEra}, +}; pub use pallet::*; pub use types::*; @@ -81,6 +85,7 @@ mod benchmarking; #[cfg(test)] mod tests; +mod tests; pub mod weights; @@ -93,7 +98,7 @@ pub mod pallet { use super::*; use codec::EncodeLike; - use common_primitives::capacity::StakingRewardsProvider; + use common_primitives::capacity::{StakingType}; use frame_support::{pallet_prelude::*, Twox64Concat}; use frame_system::pallet_prelude::*; use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay}; @@ -761,9 +766,10 @@ impl Pallet { } } - pub fn payout_eligible(account_id: T::AccountId) -> boolean { + /// Returns whether `account_id` may claim and and be paid token rewards. + pub fn payout_eligible(account_id: T::AccountId) -> bool { let _staking_account = - Self::get_staking_account_for(unstaker).ok_or(Error::::StakingAccountNotFound)?; + Self::get_staking_account_for(account_id).ok_or(Error::::StakingAccountNotFound); false } diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index cda41f03c9..c703924533 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -10,6 +10,7 @@ use sp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Saturatin use common_primitives::capacity::StakingType; #[cfg(any(feature = "runtime-benchmarks", test))] use sp_std::vec::Vec; +use common_primitives::node::{AccountId, Hash}; /// The type used for storing information about staking details. #[derive( From 1cc2f1d2dc3d92a635845f152594a2b21e9ae421 Mon Sep 17 00:00:00 2001 From: Shannon Wells Date: Wed, 7 Jun 2023 15:38:28 -0700 Subject: [PATCH 03/11] Implement Staking Reward Eras basics (#1589) Implement the basic functionality of tracking and rotating Reward Era. Closes #1567 Does not include anything to do with the Reward Pool. - [x] Chain spec updated - [x] Design doc(s) updated - [x] Tests added --- pallets/capacity/src/tests/eras_tests.rs | 2 +- pallets/capacity/src/types.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pallets/capacity/src/tests/eras_tests.rs b/pallets/capacity/src/tests/eras_tests.rs index 8f07875897..52c9e7712a 100644 --- a/pallets/capacity/src/tests/eras_tests.rs +++ b/pallets/capacity/src/tests/eras_tests.rs @@ -17,7 +17,7 @@ fn start_new_era_if_needed() { system_run_to_block(19); run_to_block(20); current_era_info = CurrentEraInfo::::get(); - assert_eq!(current_era_info.era_index, 3u32); + assert_eq!(current_era_info.era_index, 3u32); assert_eq!(current_era_info.started_at, 20u32); }) } diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index c703924533..e084a9d49b 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -372,6 +372,17 @@ where pub started_at: BlockNumber, } +/// The information needed to track a Reward Era +#[derive( + PartialEq, Eq, Clone, Default, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, +)] +pub struct RewardEraInfo { + /// the index of this era + pub current_era: RewardEra, + /// the starting block of this era + pub era_start: BlockNumber, +} + /// Needed data about a RewardPool for a given RewardEra. /// The total_reward_pool balance for the previous era is set when a new era starts, /// based on total staked token at the end of the previous era, and remains unchanged. From 0a9e8a91ae17e01dfcfe119b193a03f33fc10b52 Mon Sep 17 00:00:00 2001 From: Shannon Wells Date: Wed, 14 Jun 2023 16:07:25 -0700 Subject: [PATCH 04/11] Feat/staking rewards rewards provider #1572 (#1598) The goal of this PR is to implement a really basic version of the StakingRewardsProvider in the Capacity pallet and in the test mock, neither of which is actively used. Closes #1572 Does not include anything to do with setting and storing RewardPoolInfo when each new Era starts. - [x] Design doc(s) updated - [x] Tests added --- pallets/capacity/src/tests/stake_and_deposit_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/capacity/src/tests/stake_and_deposit_tests.rs b/pallets/capacity/src/tests/stake_and_deposit_tests.rs index 3f166bbd48..40072172f2 100644 --- a/pallets/capacity/src/tests/stake_and_deposit_tests.rs +++ b/pallets/capacity/src/tests/stake_and_deposit_tests.rs @@ -2,7 +2,7 @@ use super::{mock::*, testing_utils::*}; use crate::{BalanceOf, CapacityDetails, Error, Event, StakingAccountDetails}; use common_primitives::{ capacity::{ - Nontransferable, StakingType, + Nontransferable, StakingType::{MaximumCapacity, ProviderBoost}, }, msa::MessageSourceId, From e7735061c8e6a89c557675e2fc397218dc8655b6 Mon Sep 17 00:00:00 2001 From: Shannon Wells Date: Fri, 7 Jul 2023 15:30:03 -0700 Subject: [PATCH 05/11] change staking target extrinsic, closes #1570 (#1623) The goal of this PR is to implement the `change_staking_target` extrinsic as specified in the Staking Rewards design document. The design document is updated as part of this PR to account for needed changes discovered during this implementation phase. Closes #1570 --- ...capacity_staking_rewards_implementation.md | 321 ++++++------------ 1 file changed, 100 insertions(+), 221 deletions(-) diff --git a/designdocs/capacity_staking_rewards_implementation.md b/designdocs/capacity_staking_rewards_implementation.md index 24f8b02af5..ac8f762c16 100644 --- a/designdocs/capacity_staking_rewards_implementation.md +++ b/designdocs/capacity_staking_rewards_implementation.md @@ -1,26 +1,4 @@ # Capacity Staking Rewards Implementation - -## Overview -Staking Capacity for rewards is a new feature which allows token holders to stake FRQCY and split the staking -rewards with a Provider they choose. The Provider receives a small reward in Capacity -(which is periodically replenished), and the staker receives a periodic return in FRQCY token. -The amount of Capacity that the Provider would receive in such case is a fraction of what they would get from a -`MaximumCapacity` stake. - -The period of Capacity replenishment - the `Epoch` - and the period of token reward - the `RewardEra`- are different. -Epochs much necessarily be much shorter than rewards because Capacity replenishment needs to be multiple times a day to meet the needs of a high traffic network, and to allow Providers the ability to delay transactions to a time of day with lower network activity if necessary. -Reward eras need to be on a much longer scale, such as every two weeks, because there are potentially orders of magnitude more stakers, and calculating rewards is computationally more intensive than updating Capacity balances for the comparatively few Providers. -In addition, this lets the chain to store Reward history for much longer rather than forcing people to have to take steps to claim rewards. - -### Diagram -This illustrates roughly (and not to scale) how Provider Boost staking works. Just like the current staking behavior, now called Maximized staking, The Capacity generated by staking is added to the Provider's Capacity ledger immediately so it can be used right away. The amount staked is locked in Alice's account, preventing transfer. - -Provider Boost token rewards are earned only for token staked for a complete Reward Era. So Alice does not begin earning rewards until Reward Era 5 in the diagram, and this means Alice must wait until Reward Era 6 to claim rewards for Reward Era 5. Unclaimed reward amounts are actually not minted or transferred until they are claimed, and may also not be calculated until then, depending on the economic model. - -This process will be described in more detail in the Economic Model Design Document. - -![Provider boosted staking](https://github.com/LibertyDSNP/frequency/assets/502640/ffb632f2-79c2-4a09-a906-e4de02e4f348) - The proposed feature is a design for staking FRQCY token in exchange for Capacity and/or FRQCY. It is specific to the Frequency Substrate parachain. It consists of enhancements to the capacity pallet, needed traits and their implementations, and needed runtime configuration. @@ -28,70 +6,44 @@ It consists of enhancements to the capacity pallet, needed traits and their impl This does _not_ outline the economic model for Staking Rewards (also known as "Provider Boosting"); it describes the economic model as a black box, i.e. an interface. ## Context and Scope: -The Frequency Transaction Payment system allows certain transactions on chain to be paid for with Capacity. Accounts that wish to pay with Capacity must: +The Frequency Transaction Payment system uses Capacity to pay for certain transactions on chain. Accounts that wish to pay with Capacity must: 1. Have an [MSA](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/accounts.md) 2. Be a [Provider](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/provider_registration.md) (see also [Provider Permissions and Grants](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/provider_permissions.md)) 3. Stake a minimum amount of FRQCY (on mainnet, UNIT on Rococo testnet) token to receive [Capacity](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/capacity.md). # Problem Statement -This document outlines how to implement the Staking for Rewards feature described in [Capacity Staking Rewards Economic Model (TBD)](TBD). -It does not give regard to what the economic model actually is, since that is yet to be determined. +This document outlines how to implement the Staking for Rewards feature described in [Capacity Staking Rewards Economic Model (TBD)](TBD), without, at this time, regard to what the economic model actually is. ## Glossary -1. **FRQCY**: the native token of Frequency, a Substrate parachain in the Polkdaot blockhain ecosystem. +1. **FRQCY**: the native token of the Frequency blockchain 1. **Capacity**: the non-transferrable utility token which can be used only to pay for certain Frequency transactions. 1. **Account**: a Frequency System Account controlled by a private key and addressed by a public key, having at least a minimum balance (currently 0.01 FRQCY). 1. **Stake** (verb): to lock some amount of a token against transfer for a period of time in exchange for some reward. -1. **RewardEra**: the time period (TBD in blocks) that Staking Rewards are based upon. `RewardEra` is to distinguish it easily from Substrate's staking pallet Era, or the index of said time period. +1. **RewardEra**: the time period (TBD in blocks or Capacity Epochs) that Staking Rewards are based upon. RewardEra is to distinguish it easily from Substrate's staking pallet Era, or the index of said time period. 1. **Staking Reward**: a per-RewardEra share of a staking reward pool of FRQCY tokens for a given staking account. -1. **Reward Pool**: a fixed amount of FRQCY that can be minted for rewards each RewardEra and distributed to stakers. +1. **Reward Pool**: a fixed amount of FRQCY that can be minted for rewards each RewardEra and distributed to stakers. 1. **StakingRewardsProvider**: a trait that encapsulates the economic model for staking rewards, providing functionality for calculating the reward pool and staking rewards. ## Staking Token Rewards ### StakingAccountDetails updates -New fields are added. The field **`last_rewarded_at`** is to keep track of the last time rewards were claimed for this Staking Account. -MaximumCapacity staking accounts MUST always have the value `None` for `last_rewarded_at`. -Finally, `stake_change_unlocking`, is added, which stores an `UnlockChunk` when a staking account has changed. -targets for some amount of funds. This is to prevent retarget spamming. - -This will be a V2 of this storage and original StakingAccountDetails will need to be migrated. +New fields are added. The field `last_rewarded_at` is to keep track of the last time rewards were claimed for this Staking Account. +MaximumCapacity staking accounts MUST always have the value `None` for `last_rewarded_at`. This should be the default value also. +`MaximumCapacity` is also the default value for `staking_type` and should map to 0. +Finally, `stake_change_unlocking`, a BoundedVec is added which tracks the chunks of when a staking account has changed targets for some amount of funds. ```rust -pub struct StakingAccountDetailsV2 { +pub struct StakingAccountDetails { pub active: BalanceOf, pub total: BalanceOf, pub unlocking: BoundedVec, T::EpochNumber>, T::MaxUnlockingChunks>, /// The number of the last StakingEra that this account's rewards were claimed. pub last_rewards_claimed_at: Option, // NEW None means never rewarded, Some(RewardEra) means last rewarded RewardEra. + /// What type of staking this account is doing + pub staking_type: StakingType, // NEW /// staking amounts that have been retargeted are prevented from being retargeted again for the /// configured Thawing Period number of blocks. - pub stake_change_unlocking: BoundedVec, T::RewardEra>, T::MaxUnlockingChunks>, // NEW -} -``` - -### StakingTargetDetails updates, StakingHistory -A new field, `staking_type` is added to indicate the type of staking the Account holder is doing in relation to this target. -Staking type may be `MaximumCapacity` or `ProviderBoost`. `MaximumCapacity` is the default value for `staking_type` and maps to 0. - -```rust -/// A per-reward-era record for StakingAccount total_staked amount. -pub struct StakingHistory { // NEW - total_staked: Balance, - reward_era: RewardEra, -} - -#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] -#[scale_info(skip_type_params(T))] -pub struct StakingTargetDetails { - /// The total amount of tokens that have been targeted to the MSA. - pub amount: BalanceOf, - /// The total Capacity that an MSA received. - pub capacity: BalanceOf, - /// The type of staking, which determines ultimate capacity per staked token. - pub staking_type: StakingType, // NEW - /// total staked amounts for each past era, up to StakingRewardsPastErasMax eras. - pub staking_history: BoundedVec, T::RewardEra>, T::StakingRewardsPastErasMax>, // NEW + pub stake_change_unlocking: BoundedVec, EraOf>, T::MaxUnlockingChunks> // NEW } ``` @@ -99,49 +51,40 @@ pub struct StakingTargetDetails { Changes the thaw period to begin at the first block of next RewardEra instead of immediately. ### Changes to extrinsics -#### stake -The parameters for the `stake` extrinsic remain the same and the behavior is the same, in that this creates or adds -more token to a staker-target relationship with type `MaximiumCapacity`. -However, if one calls `stake` with a `target` that `origin` already has a staker-target relationsip with, -it is _not_ a `MaximumCapacity` staking type, it will error with `Error::CannotChangeStakingType`. - -#### unstake -The unstake parameters are the same, and unstake behavior is the same for `MaximumCapacity` as before, however -for a `ProviderBoost` staker-target relationship, the behavior must be different. While it's not feasible to -store either `reward_pool` history or individual staking reward history indefinitely, it still may be lengthy -enough that having to calculate _all_ unclaimed rewards for what could be numerous accounts in one block -could make a block heavier than desired. Therefore there must be a limit limit on how many eras -one can claim rewards for. This value will likely be a pallet constant. The logic would be: - - * If a ProviderBoost stake is `payout_eligible`, - * check whether their last payout era is recent enough to pay out all rewards at once. - * if so, first pay out all rewards and then continue with rest of unstaking code as is - * if not, emit error `MustFirstClaimRewards`, `UnclaimedRewardsOverTooManyEras` or something like that. - Don't use `EraOutOfRange` because it will overload the meaning of that error; needs to be something more specific. - * If not payout eligible, - * check whether the last payout era is the current one. - * if so, all rewards have been claimed, so continue with rest of unstaking code as is, - * if not, it means they have too many unlocking chunks so they'll have to wait. - the unstaking code - will catch this anyway and emit `MaxUnlockingChunksExceeded` - ```rust +pub fn stake( + origin: OriginFor, + target: MessageSourceId, + amount: BalanceOf, + staking_type: StakingType // NEW +) -> DispatchResult { + /// NEW BEHAVIOR: + // if the account is new, save the new staking type + // if not new and staking type is different, Error::CannotChangeStakingType +} + pub fn unstake( origin: OriginFor, target: MessageSourceId, requested_amount: BalanceOf, -) -> DispatchResult {} - +) -> DispatchResult { + // NEW BEHAVIOR: + // If StakingType is RewardsType + // If payout_eligible, + // check whether their last payout era is recent enough to pay out all rewards at once. + // if so, first pay out all rewards and then continue with rest of unstaking code as is + // if not, emit error "MustFirstClaimUnclaimedRewards", "UnclaimedRewardsOverTooManyEras" or something like that + // If not payout eligible, + // check whether the last payout era is the current one. + // if so, all rewards have been claimed, so continue with rest of unstaking code as is, + // + // otherwise, they have too many unlocking chunks so they'll have to wait. - the unstaking code + // will catch this anyway and emit `MaxUnlockingChunksExceeded` +} ``` ### NEW: StakingRewardsProvider - Economic Model trait -This one is not yet determined, however there are certain functions that will definitely be needed. -The rewards system will still need to know the `reward_pool_size`. - -The struct and method for claiming rewards is probably going to change. -The `staking_reward_total` for a given staker may not be calculable by the node, depending on the complexity of the -economic rewards model. -It's possible that it would be calculated via some app with access to the staker's wallet, and submitted as a proof -with a payload. -In that case the `validate_staking_reward_claim` is more likely to be part of the trait. +This one is most likely to change, however there are certain functions that will definitely be needed. +The struct and method for claiming rewards is probably going to change, but the rewards system will still need to know the `reward_pool_size` and the `staking_reward_total` for a given staker. ```rust use std::hash::Hash; @@ -165,7 +108,6 @@ pub trait StakingRewardsProvider { /// Return the total unclaimed reward in token for `account_id` for `fromEra` --> `toEra`, inclusive /// Errors: /// - EraOutOfRange when fromEra or toEra are prior to the history retention limit, or greater than the current RewardEra. - /// May not be possible depending on economic model complexity. fn staking_reward_total(account_id: T::AccountId, fromEra: T::RewardEra, toEra: T::RewardEra); /// Validate a payout claim for `account_id`, using `proof` and the provided `payload` StakingRewardClaim. @@ -175,6 +117,17 @@ pub trait StakingRewardsProvider { } ``` +### NEW: StakingType enum +```rust +pub enum StakingType { + /// Staking account targets Providers for capacity only, no token reward + MaximizedCapacity, + /// Staking account targets Providers and splits reward between capacity to the Provider + /// and token for the account holder + Rewards, +} +``` + ### NEW: Config items ```rust pub trait Config: frame_system::Config { @@ -196,35 +149,24 @@ pub trait Config: frame_system::Config { type EraLength: Get; /// The maximum number of eras over which one can claim rewards type StakingRewardsPastErasMax: Get; - /// The trait providing the ProviderBoost economic model calculations and values + type RewardsProvider: StakingRewardsProvider; }; ``` -### NEW: RewardPoolInfo, RewardPoolHistory -Information about the reward pool for a given Reward Era and how it's stored. The size of this pool is limited to -`StakingRewardsPastErasMax` but is stored as a CountedStorageMap instead of a BoundedVec for performance reasons: -* claiming rewards for the entire history will be unlikely to be allowed. Iterating over a much smaller range is more performant -* Fetching/writing the entire history every block could affect block times. Instead, once per block, retrieve the latest record, delete the earliest record and insert a new one +### NEW: RewardPoolInfo +This is the necessary information about the reward pool for a given Reward Era and how it's stored. ```rust -pub struct RewardPoolInfo { - /// the total staked for rewards in the associated RewardEra - pub total_staked_token: Balance, - /// the reward pool for this era - pub total_reward_pool: Balance, - /// the remaining rewards balance to be claimed - pub unclaimed_balance: Balance, +pub struct RewardPoolInfo { } - /// Reward Pool history #[pallet::storage] #[pallet::getter(fn get_reward_pool_for_era)] -pub type StakingRewardPool = ; +pub type StakingRewardPool = ; ``` ### NEW: CurrentEra, RewardEraInfo Incremented, like CurrentEpoch, tracks the current RewardEra number and the block when it started. -Storage is whitelisted because it's accessed every block and would improperly adversely impact all benchmarks. ```rust #[pallet::storage] #[pallet::whitelist_storage] @@ -250,105 +192,60 @@ pub enum Error { EraOutOfRange, /// Rewards were already paid out for the specified Era range IneligibleForPayoutInEraRange, - /// Attempted to retarget but from and to Provider MSA Ids were the same - CannotRetargetToSameProvider, - /// Rewards were already paid out this era - AlreadyClaimedRewardsThisEra, } ``` ### NEW Extrinsics -This is the most undecided portion of this design and depends strongly on the chosen economic model for Provider Boosting. -There are generally two forms that claiming a staking reward could take, and this depends on whether it's possible to -calculate rewards on chain at all. - -Regardless, on success, the claimed rewards are minted and transferred as locked token to the origin, with the existing -unstaking thaw period for withdrawal (which simply unlocks thawed token amounts as before). -There is no chunk added; instead the existing unstaking thaw period is applied to last_rewards_claimed_at in StakingAccountDetails. - -Forcing stakers to wait a thaw period for every claim is an incentive to claim rewards sooner than later, leveling out -possible inflationary effects and helping prevent unclaimed rewards from expiring. -The thaw period must be short enough for all rewards to be claimed before rewards history would end. -Therefore, it's possible that a complete separate reward claim thaw period would need to be used. - -For all forms of claim_staking_reward, the event `StakingRewardClaimed` is emitted with the parameters of the extrinsic. - -#### provider_boost(origin, target, amount) -Like `stake`, except this extrinsic creates or adds staked token to a `ProviderBoost` type staker-target relationship. -In the case of an increase in stake, `staking_type` MUST be a `ProviderBoost` type, or else it will error with `Error::CannotChangeStakingType`. -```rust -pub fn provider_boost( - origin: OriginFor, - target: MessageSourceId, - amount: BalanceOf, -) -> DispatchResult {} -``` - -#### 1. claim_staking_reward(origin, from_era, to_era), simple economic model -In the case of a simple economic model such as a fixed rate return, reward calculations may be done on chain - -within discussed limits. -```rust -/// Claim staking rewards from `from_era` to `to_era`, inclusive. -/// from_era: if None, since last_reward_claimed_at -/// to_era: if None, to CurrentEra - 1 -/// Errors: -/// - NotAStakingAccount: if Origin does not own the StakingRewardDetails in the claim. -/// - IneligibleForPayoutInEraRange: if rewards were already paid out in the provided RewardEra range -/// - EraOutOfRange: -/// - if `from_era` is earlier than history storage -/// - if `to_era` is >= current era -/// - if `to_era` - `from_era` > StakingRewardsPastErasMax -#[pallet::call_index(n)] -pub fn claim_staking_reward( - origin: OriginFor, - from_era: Option, - to_era: Option -); -``` - -#### 2. claim_staking_reward(origin,proof,payload) -TBD whether this is the form for claiming rewards. -This could be the form if calculations are done off chain and submitted for validation. - -```rust +1. *claim_staking_reward*, first version + a. `claim_staking_reward(origin,proof,payload)` + ```rust + /// TBD whether this is the form for claiming rewards. This could be the form if calculations are + /// done off chain and submitted for validation. /// Validates the reward claim. If validated, mints token and transfers to Origin. /// Errors: /// - NotAStakingAccount: if Origin does not own the StakingRewardDetails in the claim. /// - StakingRewardClaimInvalid: if validation of calculation fails /// - IneligibleForPayoutInEraRange: if rewards were already paid out in the provided RewardEra range - /// - EraOutOfRange: - /// - if `from_era` is earlier than history storage - /// - if `to_era` is >= current era - /// - if `to_era` - `from_era` > StakingRewardsPastErasMax + /// - EraOutOfRange: if one or both of the StakingRewardClaim eras are invalid + /// `proof` - the Merkle proof for the reward claim #[pallet::call_index(n)] pub fn claim_staking_reward( origin: OriginFor, - /// `proof` - the Merkle proof for the reward claim proof: Hash, - /// The staking reward claim payload for which the proof was generated payload: StakingRewardClaim ); -``` -#### 3. change_staking_target(origin, from, to, amount) + ``` + b. *claim_staking_reward*, alternate version + ```rust + /// An alternative, depending on staking reward economic model. This could be the form if calculations are done on chain. + /// from_era: if None, since last_reward_claimed_at + /// to_era: if None, to CurrentEra - 1 + /// Errors: + /// - NotAStakingAccount: if Origin does not own the StakingRewardDetails in the claim. + /// - IneligibleForPayoutInEraRange: if rewards were already paid out in the provided RewardEra range + /// - EraOutOfRange: if one or both of the eras specified are invalid + #[pallet::call_index(n)] + pub fn claim_staking_reward( + origin: OriginFor, + from_era: Option, + to_era: Option + ); + ``` + Both emit events `StakingRewardClaimed` with the parameters of the extrinsic. + +2. **change_staking_target(origin, from, to, amount)** Changes a staking account detail's target MSA Id to a new one by `amount` Rules for this are similar to unstaking; if `amount` would leave less than the minimum staking amount for the `from` target, the entire amount is retargeted. -No more than `T::MaxUnlockingChunks` staking amounts may be retargeted within this Thawing Period. +No more than T::MaxUnlockingChunks staking amounts may be retargeted within this Thawing Period. Each call creates one chunk. Emits a `StakingTargetChanged` event with the parameters of the extrinsic. + ```rust -/// Sets the target of the staking capacity to a new target. -/// This adds a chunk to `StakingAccountDetails.stake_change_unlocking chunks`, up to `T::MaxUnlockingChunks`. -/// The staked amount and Capacity generated by `amount` originally targeted to the `from` MSA Id is reassigned to the `to` MSA Id. -/// Does not affect unstaking process or additional stake amounts. -/// Changing a staking target to a Provider when Origin has nothing staked them will retain the staking type. -/// Changing a staking target to a Provider when Origin has any amount staked to them will error if the staking types are not the same. -/// ### Errors -/// - [`Error::NotAStakingAccount`] if origin does not have a staking account -/// - [`Error::MaxUnlockingChunksExceeded`] if `stake_change_unlocking_chunks` == `T::MaxUnlockingChunks` -/// - [`Error::StakerTargetRelationshipNotFound`] if `from` is not a target for Origin's staking account. -/// - [`Error::StakingAmountBelowMinimum`] if `amount` to retarget is below the minimum staking amount. -/// - [`Error::InsufficientStakingBalance`] if `amount` to retarget exceeds what the staker has targeted to `from` MSA Id. -/// - [`Error::InvalidTarget`] if `to` does not belong to a registered Provider. -/// - [`Error::CannotChangeStakingType`] if origin already has funds staked for `to` and the staking type for `from` is different. +/// Errors: +/// - MaxUnlockingChunksExceeded if 'from' target staking amount is still thawing in the staking unlock chunks (either type) +/// - StakerTargetRelationshipNotFound` if `from` is not a staking target for Origin. This also covers when account's MSA is not staking anything at all or account has no MSA +/// - StakingAmountBelowMinimum if amount to retarget is below the minimum staking amount. +/// - InsufficientStakingBalance if amount to retarget exceeds what the staker has targeted to the `from` MSA Id. +/// - InvalidTarget if `to` is not a Registered Provider. #[pallet:call_index(n+1)] // n = current call index in the pallet pub fn change_staking_target( origin: OriginFor, @@ -359,37 +256,19 @@ pub fn change_staking_target( ``` ### NEW: Capacity pallet helper function -#### payout_eligible -Returns whether `account_id` can claim a reward at all. -This function will return false if there is no staker-target relationship. -Staking accounts may claim rewards: -* ONCE per RewardEra, -* Only for funds staked for a complete RewardEra, i.e. the balance at the end of the Era, -* Must wait for the thaw period to claim rewards again (see `last_rewards_claimed_at`) ```rust +/// Return whether `account_id` can claim a reward. Staking accounts may not claim a reward more than once +/// per RewardEra, may not claim rewards before a complete RewardEra has been staked, and may not claim more rewards past +/// the number of `MaxUnlockingChunks`. +/// Errors: +/// NotAStakingAccount if account_id has no StakingAccountDetails in storage. fn payout_eligible(account_id: AccountIdOf) -> bool; ``` -### NEW RPCS +### NEW RPC There are no custom RPCs for the Capacity pallet, so that work will need to be done first. - -The form of this will depend on whether the rewards calculation for an individual account is done by the node or externally -with a submitted proof. If externally, then unclaimed rewards would not include an earned amount. - ```rust -pub struct UnclaimedRewardInfo { - /// The Reward Era for which this reward was earned - reward_era: RewardEra, - /// An ISO8701 string, UTC, estimated using current block time, and the number of blocks between - /// the current block and the block when this era's RewardPoolInfo would be removed from StakingRewardPool history - expires_at: string, - /// The amount staked in this era - staked_amount: BalanceOf, - /// The amount in token of the reward (only if it can be calculated using only on chain data) - earned_amount: BalanceOf -} - -/// Check what unclaimed rewards origin has and how long they have left to claim them -/// If no unclaimed rewards, returns empty list. -fn check_for_unclaimed_rewards(origin: OriginFor) -> Vec; +/// RPC access to the pallet function by the same name +pub fn payout_eligible(account_id: AccountId) -> bool; ``` + From 1f7396918b84b3dac1354cf5d605f678839a33df Mon Sep 17 00:00:00 2001 From: shannonwells Date: Fri, 6 Oct 2023 17:06:35 -0700 Subject: [PATCH 06/11] saving current work WIP --- pallets/capacity/src/lib.rs | 80 ++- .../src/tests/change_staking_target_tests.rs | 644 +++++++++--------- pallets/capacity/src/tests/eras_tests.rs | 31 +- pallets/capacity/src/tests/mock.rs | 6 +- .../src/tests/rewards_provider_tests.rs | 3 +- pallets/capacity/src/tests/testing_utils.rs | 16 + pallets/capacity/src/tests/unstaking_tests.rs | 2 +- pallets/capacity/src/types.rs | 6 +- 8 files changed, 415 insertions(+), 373 deletions(-) diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index 583dafe2c9..2fc5ebc8dd 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -180,7 +180,7 @@ pub mod pallet { /// The maximum number of eras over which one can claim rewards #[pallet::constant] - type StakingRewardsPastErasMax: Get; + type StakingRewardsPastErasMax: Get; /// The StakingRewardsProvider used by this pallet in a given runtime type RewardsProvider: StakingRewardsProvider; @@ -189,7 +189,7 @@ pub mod pallet { /// thaw chunk to expire. If the staker has called change_staking_target MaxUnlockingChunks /// times, then at least one of the chunks must have expired before the next call /// will succeed. - type ChangeStakingTargetThawEras: Get; + type ChangeStakingTargetThawEras: Get; } /// Storage for keeping a ledger of staked token amounts for accounts. @@ -255,10 +255,8 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn get_reward_pool_for_era)] pub type StakingRewardPool = - StorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo>>; + CountedStorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo>>; - // Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and - // method. #[pallet::pallet] pub struct Pallet(_); @@ -359,6 +357,8 @@ pub mod pallet { IneligibleForPayoutInEraRange, /// Attempted to retarget but from and to Provider MSA Ids were the same CannotRetargetToSameProvider, + /// Rewards were already paid out this era + AlreadyClaimedRewardsThisEra, } #[pallet::hooks] @@ -490,6 +490,8 @@ pub mod pallet { /// This adds a chunk to `StakingAccountDetails.stake_change_unlocking chunks`, up to `T::MaxUnlockingChunks`. /// The staked amount and Capacity generated by `amount` originally targeted to the `from` MSA Id is reassigned to the `to` MSA Id. /// Does not affect unstaking process or additional stake amounts. + /// Changing a staking target to a Provider when Origin has nothing staked them will retain the staking type. + /// Changing a staking target to a Provider when Origin has any amount staked to them will error if the staking types are not the same. /// ### Errors /// - [`Error::NotAStakingAccount`] if origin does not have a staking account /// - [`Error::MaxUnlockingChunksExceeded`] if `stake_change_unlocking_chunks` == `T::MaxUnlockingChunks` @@ -497,6 +499,7 @@ pub mod pallet { /// - [`Error::StakingAmountBelowMinimum`] if `amount` to retarget is below the minimum staking amount. /// - [`Error::InsufficientStakingBalance`] if `amount` to retarget exceeds what the staker has targeted to `from` MSA Id. /// - [`Error::InvalidTarget`] if `to` does not belong to a registered Provider. + /// - [`Error::CannotChangeStakingType`] if origin already has funds staked for `to` and the staking type for `from` is different. #[pallet::call_index(4)] #[pallet::weight(T::WeightInfo::unstake())] pub fn change_staking_target( @@ -774,18 +777,38 @@ impl Pallet { } fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight { - let current_era_info: RewardEraInfo = Self::get_current_era(); - if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { - CurrentEraInfo::::set(RewardEraInfo { + let current_era_info: RewardEraInfo = Self::get_current_era(); // 1r + if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { // 1r + let new_era_info = RewardEraInfo { era_index: current_era_info.era_index.saturating_add(One::one()), started_at: current_block, - }); - // TODO: modify reads/writes as needed when RewardPoolInfo stuff is added + }; + + let current_reward_pool = Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r + let past_eras_max = T::StakingRewardsPastErasMax::get(); + let entries: u32 = StakingRewardPool::::count(); + if past_eras_max.eq(&entries.into()) { // 2r + let current_era = Self::get_current_era().era_index; + let earliest_era = current_era.saturating_sub(past_eras_max); + StakingRewardPool::::remove(earliest_era); // 1w + } + CurrentEraInfo::::set(new_era_info); // 1w + + // let msa_handle = T::HandleProvider::get_handle_for_msa(msa_id); + let total_reward_pool = + T::RewardsProvider::reward_pool_size(current_reward_pool.total_staked_token.clone()); + let new_reward_pool = RewardPoolInfo { + total_staked_token: current_reward_pool.total_staked_token, + total_reward_pool, + unclaimed_balance: total_reward_pool.clone(), + }; + StakingRewardPool::::insert(new_era_info.era_index.clone(), new_reward_pool); // 1w + T::WeightInfo::on_initialize() - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) } else { - T::DbWeight::get().reads(2).saturating_add(RocksDbWeight::get().writes(1)) + T::DbWeight::get().reads(2) } } @@ -806,7 +829,8 @@ impl Pallet { Self::get_staking_account_for(staker).ok_or(Error::::NotAStakingAccount)?; let current_era: T::RewardEra = Self::get_current_era().era_index; - let thaw_at = current_era.saturating_add(T::ChangeStakingTargetThawEras::get().into()); + let thaw_eras = T::ChangeStakingTargetThawEras::get(); + let thaw_at = current_era.saturating_add(thaw_eras); staking_account_details.update_stake_change_unlocking(amount, &thaw_at, ¤t_era)?; Self::set_staking_account(staker, &staking_account_details); Ok(()) @@ -824,15 +848,16 @@ impl Pallet { let capacity_withdrawn = Self::reduce_capacity(staker, *from_msa, *amount)?; let mut to_msa_target = Self::get_target_for(staker, to_msa).unwrap_or_default(); + + if to_msa_target.amount.is_zero() { + to_msa_target.staking_type = staking_type.clone(); + } else { + ensure!(to_msa_target.staking_type.ne(staking_type), Error::::CannotChangeStakingType); + } to_msa_target .deposit(*amount, capacity_withdrawn) .ok_or(ArithmeticError::Overflow)?; - // TODO: document - // if someone wants to switch staking type they must unstake completely and restake regardless of - // whether it is with an existing or new provider. - to_msa_target.staking_type = staking_type.clone(); - let mut capacity_details = Self::get_capacity_for(to_msa).unwrap_or_default(); capacity_details .deposit(amount, &capacity_withdrawn) @@ -935,19 +960,15 @@ impl StakingRewardsProvider for Pallet { // Calculate the size of the reward pool for the current era, based on current staked token // and the other determined factors of the current economic model - fn reward_pool_size() -> Result, DispatchError> { - let current_era_info = CurrentEraInfo::::get(); - let current_staked = - StakingRewardPool::::get(current_era_info.era_index).unwrap_or_default(); - if current_staked.total_staked_token.is_zero() { - return Ok(BalanceOf::::zero()) + fn reward_pool_size(total_staked: BalanceOf) -> BalanceOf { + if total_staked.is_zero() { + return BalanceOf::::zero() } // For now reward pool size is set to 10% of total staked token - Ok(current_staked - .total_staked_token + total_staked .checked_div(&BalanceOf::::from(10u8)) - .unwrap_or_default()) + .unwrap_or_default() } // Performs range checks plus a reward calculation based on economic model for the era range @@ -956,9 +977,8 @@ impl StakingRewardsProvider for Pallet { from_era: T::RewardEra, to_era: T::RewardEra, ) -> Result, DispatchError> { - let max_eras = T::RewardEra::from(T::StakingRewardsPastErasMax::get()); let era_range = from_era.saturating_sub(to_era); - ensure!(era_range.le(&max_eras), Error::::EraOutOfRange); + ensure!(era_range.le(&T::StakingRewardsPastErasMax::get()), Error::::EraOutOfRange); ensure!(from_era.le(&to_era), Error::::EraOutOfRange); let current_era_info = Self::get_current_era(); ensure!(to_era.lt(¤t_era_info.era_index), Error::::EraOutOfRange); diff --git a/pallets/capacity/src/tests/change_staking_target_tests.rs b/pallets/capacity/src/tests/change_staking_target_tests.rs index c9ee0d7b12..e52ee039de 100644 --- a/pallets/capacity/src/tests/change_staking_target_tests.rs +++ b/pallets/capacity/src/tests/change_staking_target_tests.rs @@ -1,312 +1,298 @@ -use super::{mock::*, testing_utils::*}; +use super::{mock::*, testing_utils::{setup_provider, staking_events} }; use crate::{ - BalanceOf, CapacityDetails, Config, CurrentEraInfo, Error, Event, RewardEraInfo, - StakingAccountDetails, StakingAccountLedger, StakingTargetDetails, + BalanceOf, CapacityDetails, Config, CurrentEraInfo, Error, Event, RewardEraInfo, + StakingAccountDetails, StakingAccountLedger, StakingTargetDetails, }; use common_primitives::{ - capacity::{ - StakingType, - StakingType::{MaximumCapacity, ProviderBoost}, - }, - msa::MessageSourceId, + capacity::{ + StakingType, + StakingType::{MaximumCapacity, ProviderBoost}, + }, + msa::MessageSourceId, }; use frame_support::{assert_noop, assert_ok, traits::Get}; // staker is unused unless amount > 0 -fn setup_provider(staker: &u64, target: &MessageSourceId, amount: &u64, staking_type: StakingType) { - let provider_name = String::from("Cst-") + target.to_string().as_str(); - register_provider(*target, provider_name); - if amount.gt(&0u64) { - assert_ok!(Capacity::stake( - RuntimeOrigin::signed(staker.clone()), - *target, - *amount, - staking_type.clone() - )); - let target = Capacity::get_target_for(staker, target).unwrap(); - assert_eq!(target.amount, *amount); - assert_eq!(target.staking_type, staking_type); - } -} - type TestCapacityDetails = CapacityDetails, u32>; type TestTargetDetails = StakingTargetDetails; fn assert_capacity_details( - msa_id: MessageSourceId, - remaining_capacity: u64, - total_tokens_staked: u64, - total_capacity_issued: u64, + msa_id: MessageSourceId, + remaining_capacity: u64, + total_tokens_staked: u64, + total_capacity_issued: u64, ) { - let expected_from_details: TestCapacityDetails = CapacityDetails { - remaining_capacity, - total_tokens_staked, - total_capacity_issued, - last_replenished_epoch: 0, - }; - let from_capacity_details: TestCapacityDetails = Capacity::get_capacity_for(msa_id).unwrap(); - assert_eq!(from_capacity_details, expected_from_details); + let expected_from_details: TestCapacityDetails = CapacityDetails { + remaining_capacity, + total_tokens_staked, + total_capacity_issued, + last_replenished_epoch: 0, + }; + let from_capacity_details: TestCapacityDetails = Capacity::get_capacity_for(msa_id).unwrap(); + assert_eq!(from_capacity_details, expected_from_details); } fn assert_target_details( - staker: u64, - msa_id: MessageSourceId, - amount: u64, - capacity: u64, - staking_type: StakingType, + staker: u64, + msa_id: MessageSourceId, + amount: u64, + capacity: u64, + staking_type: StakingType, ) { - let expected_from_target_details: TestTargetDetails = - StakingTargetDetails { amount, capacity, staking_type }; - let from_target_details = Capacity::get_target_for(staker, msa_id).unwrap(); - assert_eq!(from_target_details, expected_from_target_details); + let expected_from_target_details: TestTargetDetails = + StakingTargetDetails { amount, capacity, staking_type }; + let from_target_details = Capacity::get_target_for(staker, msa_id).unwrap(); + assert_eq!(from_target_details, expected_from_target_details); } + #[test] fn do_retarget_happy_path() { - new_test_ext().execute_with(|| { - let staker = 10_000; - let from_msa: MessageSourceId = 1; - let from_amount = 600u64; - let to_amount = 300u64; - let to_msa: MessageSourceId = 2; - let staking_type = ProviderBoost; - setup_provider(&staker, &from_msa, &from_amount, staking_type.clone()); - setup_provider(&staker, &to_msa, &to_amount, staking_type.clone()); + new_test_ext().execute_with(|| { + let staker = 10_000; + let from_msa: MessageSourceId = 1; + let from_amount = 600u64; + let to_amount = 300u64; + let to_msa: MessageSourceId = 2; + let staking_type = ProviderBoost; + setup_provider(&staker, &from_msa, &from_amount, staking_type.clone()); + setup_provider(&staker, &to_msa, &to_amount, staking_type.clone()); - // retarget half the stake to to_msa - assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &to_amount, &staking_type)); + // retarget half the stake to to_msa + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &to_amount, &staking_type)); - // expect from stake amounts to be halved - assert_capacity_details(from_msa, 1, 300, 1); + // expect from stake amounts to be halved + assert_capacity_details(from_msa, 1, 300, 1); - // expect to stake amounts to be increased by the retarget amount - assert_capacity_details(to_msa, 3, 600, 3); + // expect to stake amounts to be increased by the retarget amount + assert_capacity_details(to_msa, 3, 600, 3); - assert_target_details(staker, from_msa, 300, 1, staking_type.clone()); + assert_target_details(staker, from_msa, 300, 1, staking_type.clone()); - assert_target_details(staker, to_msa, 600, 3, staking_type.clone()); - }) + assert_target_details(staker, to_msa, 600, 3, staking_type.clone()); + }) } #[test] fn do_retarget_flip_flop() { - new_test_ext().execute_with(|| { - let staker = 10_000; - let from_msa: MessageSourceId = 1; - let from_amount = 600u64; - let to_amount = 300u64; - let to_msa: MessageSourceId = 2; - setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); - - for i in 0..4 { - if i % 2 == 0 { - assert_ok!(Capacity::do_retarget( + new_test_ext().execute_with(|| { + let staker = 10_000; + let from_msa: MessageSourceId = 1; + let from_amount = 600u64; + let to_amount = 300u64; + let to_msa: MessageSourceId = 2; + setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); + + for i in 0..4 { + if i % 2 == 0 { + assert_ok!(Capacity::do_retarget( &staker, &from_msa, &to_msa, &to_amount, &ProviderBoost )); - } else { - assert_ok!(Capacity::do_retarget( + } else { + assert_ok!(Capacity::do_retarget( &staker, &to_msa, &from_msa, &to_amount, &ProviderBoost )); - } - } - assert_capacity_details(from_msa, 3, 600, 3); - assert_capacity_details(to_msa, 1, 300, 1); - }) + } + } + assert_capacity_details(from_msa, 3, 600, 3); + assert_capacity_details(to_msa, 1, 300, 1); + }) } #[test] // check that no capacity is minted or burned just by retargeting. fn check_retarget_rounding_errors() { - new_test_ext().execute_with(|| { - let staker = 10_000; - let from_msa: MessageSourceId = 1; - let from_amount = 666u64; - let to_amount = 301u64; - let to_msa: MessageSourceId = 2; - - setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); - assert_capacity_details(from_msa, 3, 666, 3); - assert_capacity_details(to_msa, 1, 301, 1); - // 666+301= 967, 3+1=4 - - assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &301u64, &ProviderBoost)); - assert_capacity_details(to_msa, 3, 602, 3); - assert_capacity_details(from_msa, 1, 365, 1); - // 602+365 = 967, 3+1 = 4 - - assert_ok!(Capacity::do_retarget(&staker, &to_msa, &from_msa, &151u64, &ProviderBoost)); - assert_capacity_details(to_msa, 2, 451, 2); - assert_capacity_details(from_msa, 2, 516, 2); - // 451+516 = 967, 2+2 = 4 - }) + new_test_ext().execute_with(|| { + let staker = 10_000; + let from_msa: MessageSourceId = 1; + let from_amount = 666u64; + let to_amount = 301u64; + let to_msa: MessageSourceId = 2; + + setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); + assert_capacity_details(from_msa, 3, 666, 3); + assert_capacity_details(to_msa, 1, 301, 1); + // 666+301= 967, 3+1=4 + + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &301u64, &ProviderBoost)); + assert_capacity_details(to_msa, 3, 602, 3); + assert_capacity_details(from_msa, 1, 365, 1); + // 602+365 = 967, 3+1 = 4 + + assert_ok!(Capacity::do_retarget(&staker, &to_msa, &from_msa, &151u64, &ProviderBoost)); + assert_capacity_details(to_msa, 2, 451, 2); + assert_capacity_details(from_msa, 2, 516, 2); + // 451+516 = 967, 2+2 = 4 + }) } fn assert_total_capacity(msas: Vec, total: u64) { - let sum = msas - .into_iter() - .map(|a| { - let capacity_details: TestCapacityDetails = Capacity::get_capacity_for(a).unwrap(); - capacity_details.total_capacity_issued - }) - .fold(0, |a, b| a + b); - assert_eq!(total, sum); + let sum = msas + .into_iter() + .map(|a| { + let capacity_details: TestCapacityDetails = Capacity::get_capacity_for(a).unwrap(); + capacity_details.total_capacity_issued + }) + .fold(0, |a, b| a + b); + assert_eq!(total, sum); } + #[test] fn check_retarget_multiple_stakers() { - new_test_ext().execute_with(|| { - let staker_10k = 10_000; - let staker_600 = 600u64; - let staker_500 = 500u64; - let staker_400 = 400u64; - - let from_msa: MessageSourceId = 1; - let to_msa: MessageSourceId = 2; - let amt1 = 192u64; - let amt2 = 313u64; - - setup_provider(&staker_10k, &from_msa, &647u64, ProviderBoost); - setup_provider(&staker_500, &to_msa, &293u64, ProviderBoost); - assert_ok!(Capacity::stake( + new_test_ext().execute_with(|| { + let staker_10k = 10_000; + let staker_600 = 600u64; + let staker_500 = 500u64; + let staker_400 = 400u64; + + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + let amt1 = 192u64; + let amt2 = 313u64; + + setup_provider(&staker_10k, &from_msa, &647u64, ProviderBoost); + setup_provider(&staker_500, &to_msa, &293u64, ProviderBoost); + assert_ok!(Capacity::stake( RuntimeOrigin::signed(staker_600.clone()), from_msa, 479u64, MaximumCapacity )); - assert_ok!(Capacity::stake( + assert_ok!(Capacity::stake( RuntimeOrigin::signed(staker_400.clone()), to_msa, 211u64, MaximumCapacity )); - // 647 * .1 * .05 = 3 (rounded down) - // 293 * .1 * .05 = 1 (rounded down) - // 479 * .1 = 48 (rounded up) - // 211 * .1 = 21 (rounded down) - // total capacity should be 73 - assert_total_capacity(vec![from_msa, to_msa], 73); - - assert_ok!(Capacity::do_retarget(&staker_10k, &from_msa, &to_msa, &amt2, &ProviderBoost)); - assert_ok!(Capacity::do_retarget(&staker_600, &from_msa, &to_msa, &amt1, &MaximumCapacity)); - assert_ok!(Capacity::do_retarget(&staker_500, &to_msa, &from_msa, &amt1, &ProviderBoost)); - assert_ok!(Capacity::do_retarget(&staker_400, &to_msa, &from_msa, &amt1, &MaximumCapacity)); - assert_total_capacity(vec![from_msa, to_msa], 73); - }) + // 647 * .1 * .05 = 3 (rounded down) + // 293 * .1 * .05 = 1 (rounded down) + // 479 * .1 = 48 (rounded up) + // 211 * .1 = 21 (rounded down) + // total capacity should be 73 + assert_total_capacity(vec![from_msa, to_msa], 73); + + assert_ok!(Capacity::do_retarget(&staker_10k, &from_msa, &to_msa, &amt2, &ProviderBoost)); + assert_ok!(Capacity::do_retarget(&staker_600, &from_msa, &to_msa, &amt1, &MaximumCapacity)); + assert_ok!(Capacity::do_retarget(&staker_500, &to_msa, &from_msa, &amt1, &ProviderBoost)); + assert_ok!(Capacity::do_retarget(&staker_400, &to_msa, &from_msa, &amt1, &MaximumCapacity)); + assert_total_capacity(vec![from_msa, to_msa], 73); + }) } #[test] fn do_retarget_deletes_staking_target_details_if_zero_balance() { - new_test_ext().execute_with(|| { - let staker = 200u64; - let from_msa: MessageSourceId = 1; - let to_msa: MessageSourceId = 2; - let amount = 10u64; - setup_provider(&staker, &from_msa, &amount, MaximumCapacity); - setup_provider(&staker, &to_msa, &amount, MaximumCapacity); - - // stake additional to provider from another Msa, doesn't matter which type. - // total staked to from_msa is now 22u64. - assert_ok!(Capacity::stake( + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + let amount = 10u64; + setup_provider(&staker, &from_msa, &amount, MaximumCapacity); + setup_provider(&staker, &to_msa, &amount, MaximumCapacity); + + // stake additional to provider from another Msa, doesn't matter which type. + // total staked to from_msa is now 22u64. + assert_ok!(Capacity::stake( RuntimeOrigin::signed(300u64), from_msa, 12u64, MaximumCapacity )); - assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &amount, &MaximumCapacity)); - - let expected_from_details: TestCapacityDetails = CapacityDetails { - remaining_capacity: 1, - total_tokens_staked: 12, - total_capacity_issued: 1, - last_replenished_epoch: 0, - }; - let from_capacity_details: TestCapacityDetails = - Capacity::get_capacity_for(from_msa).unwrap(); - assert_eq!(from_capacity_details, expected_from_details); - - let expected_to_details: TestCapacityDetails = CapacityDetails { - remaining_capacity: 2, - total_tokens_staked: 2 * amount, - total_capacity_issued: 2, - last_replenished_epoch: 0, - }; - - let to_capacity_details = Capacity::get_capacity_for(to_msa).unwrap(); - assert_eq!(to_capacity_details, expected_to_details); - - assert!(Capacity::get_target_for(staker, from_msa).is_none()); - - let expected_to_target_details: TestTargetDetails = StakingTargetDetails { - amount: 2 * amount, - capacity: 2, - staking_type: StakingType::MaximumCapacity, - }; - let to_target_details = Capacity::get_target_for(staker, to_msa).unwrap(); - assert_eq!(to_target_details, expected_to_target_details); - - assert!(Capacity::get_target_for(staker, from_msa).is_none()); - }) + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &amount, &MaximumCapacity)); + + let expected_from_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 1, + total_tokens_staked: 12, + total_capacity_issued: 1, + last_replenished_epoch: 0, + }; + let from_capacity_details: TestCapacityDetails = + Capacity::get_capacity_for(from_msa).unwrap(); + assert_eq!(from_capacity_details, expected_from_details); + + let expected_to_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 2, + total_tokens_staked: 2 * amount, + total_capacity_issued: 2, + last_replenished_epoch: 0, + }; + + let to_capacity_details = Capacity::get_capacity_for(to_msa).unwrap(); + assert_eq!(to_capacity_details, expected_to_details); + + assert!(Capacity::get_target_for(staker, from_msa).is_none()); + + let expected_to_target_details: TestTargetDetails = StakingTargetDetails { + amount: 2 * amount, + capacity: 2, + staking_type: StakingType::MaximumCapacity, + }; + let to_target_details = Capacity::get_target_for(staker, to_msa).unwrap(); + assert_eq!(to_target_details, expected_to_target_details); + + assert!(Capacity::get_target_for(staker, from_msa).is_none()); + }) } #[test] fn change_staking_starget_emits_event_on_success() { - new_test_ext().execute_with(|| { - let staker = 200u64; - let from_msa: MessageSourceId = 1; - let from_amount = 20u64; - let to_amount = from_amount / 2; - let to_msa: MessageSourceId = 2; - setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); - - assert_ok!(Capacity::change_staking_target( + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let from_amount = 20u64; + let to_amount = from_amount / 2; + let to_msa: MessageSourceId = 2; + setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); + + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staker), from_msa, to_msa, to_amount )); - let events = staking_events(); + let events = staking_events(); - assert_eq!( - events.last().unwrap(), - &Event::StakingTargetChanged { account: staker, from_msa, to_msa, amount: to_amount } - ); - }) + assert_eq!( + events.last().unwrap(), + &Event::StakingTargetChanged { account: staker, from_msa, to_msa, amount: to_amount } + ); + }) } #[test] fn change_staking_target_errors_if_too_many_changes_before_thaw() { - new_test_ext().execute_with(|| { - let staker = 200u64; - let from_msa: MessageSourceId = 1; - let to_msa: MessageSourceId = 2; - - let max_chunks: u32 = ::MaxUnlockingChunks::get(); - let staking_amount = ((max_chunks + 2u32) * 10u32) as u64; - setup_provider(&staker, &from_msa, &staking_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &10u64, ProviderBoost); - - let retarget_amount = 10u64; - for _i in 0..(max_chunks) { - assert_ok!(Capacity::change_staking_target( + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + + let max_chunks: u32 = ::MaxUnlockingChunks::get(); + let staking_amount = ((max_chunks + 2u32) * 10u32) as u64; + setup_provider(&staker, &from_msa, &staking_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &10u64, ProviderBoost); + + let retarget_amount = 10u64; + for _i in 0..(max_chunks) { + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staker), from_msa, to_msa, retarget_amount )); - } + } - assert_noop!( + assert_noop!( Capacity::change_staking_target( RuntimeOrigin::signed(staker), from_msa, @@ -315,138 +301,138 @@ fn change_staking_target_errors_if_too_many_changes_before_thaw() { ), Error::::MaxUnlockingChunksExceeded ); - }); + }); } #[test] fn change_staking_target_garbage_collects_thawed_chunks() { - new_test_ext().execute_with(|| { - let staked_amount = 50u64; - let staking_account = 200u64; - let from_target: MessageSourceId = 3; - let to_target: MessageSourceId = 4; - setup_provider(&staking_account, &from_target, &staked_amount, ProviderBoost); - setup_provider(&staking_account, &to_target, &staked_amount, ProviderBoost); - - CurrentEraInfo::::set(RewardEraInfo { era_index: 20, started_at: 100 }); - let max_chunks = ::MaxUnlockingChunks::get(); - for _i in 0..max_chunks { - assert_ok!(Capacity::change_staking_target( + new_test_ext().execute_with(|| { + let staked_amount = 50u64; + let staking_account = 200u64; + let from_target: MessageSourceId = 3; + let to_target: MessageSourceId = 4; + setup_provider(&staking_account, &from_target, &staked_amount, ProviderBoost); + setup_provider(&staking_account, &to_target, &staked_amount, ProviderBoost); + + CurrentEraInfo::::set(RewardEraInfo { era_index: 20, started_at: 100 }); + let max_chunks = ::MaxUnlockingChunks::get(); + for _i in 0..max_chunks { + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staking_account), from_target, to_target, 10u64, )); - } - CurrentEraInfo::::set(RewardEraInfo { era_index: 25, started_at: 100 }); - assert_ok!(Capacity::change_staking_target( + } + CurrentEraInfo::::set(RewardEraInfo { era_index: 25, started_at: 100 }); + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staking_account), from_target, to_target, 10u64, )); - }) + }) } #[test] fn change_staking_target_test_parametric_validity() { - new_test_ext().execute_with(|| { - let staked_amount = 10u64; - let from_account = 200u64; - - StakingAccountLedger::::insert( - from_account, - StakingAccountDetails { - active: 20, - total: 20, - unlocking: Default::default(), - last_rewards_claimed_at: None, - stake_change_unlocking: Default::default(), - }, - ); - let from_account_not_staking = 100u64; - let from_target_not_staked: MessageSourceId = 1; - let to_target_not_provider: MessageSourceId = 2; - let from_target: MessageSourceId = 3; - let to_target: MessageSourceId = 4; - setup_provider(&from_account, &from_target_not_staked, &0u64, ProviderBoost); - setup_provider(&from_account, &from_target, &staked_amount, ProviderBoost); - setup_provider(&from_account, &to_target, &staked_amount, ProviderBoost); - - assert_ok!(Capacity::stake( + new_test_ext().execute_with(|| { + let staked_amount = 10u64; + let from_account = 200u64; + + StakingAccountLedger::::insert( + from_account, + StakingAccountDetails { + active: 20, + total: 20, + unlocking: Default::default(), + last_rewards_claimed_at: None, + stake_change_unlocking: Default::default(), + }, + ); + let from_account_not_staking = 100u64; + let from_target_not_staked: MessageSourceId = 1; + let to_target_not_provider: MessageSourceId = 2; + let from_target: MessageSourceId = 3; + let to_target: MessageSourceId = 4; + setup_provider(&from_account, &from_target_not_staked, &0u64, ProviderBoost); + setup_provider(&from_account, &from_target, &staked_amount, ProviderBoost); + setup_provider(&from_account, &to_target, &staked_amount, ProviderBoost); + + assert_ok!(Capacity::stake( RuntimeOrigin::signed(from_account), from_target, staked_amount, ProviderBoost )); - struct TestCase { - from_account: u64, - from_target: MessageSourceId, - to_target: MessageSourceId, - retarget_amount: u64, - expected_err: Error, - } - let test_cases: Vec = vec![ - // from is a provider but account is not staking to it - TestCase { - from_account, - from_target: from_target_not_staked, - to_target, - retarget_amount: staked_amount, - expected_err: Error::::StakerTargetRelationshipNotFound, - }, - // from_account is not staking at all. - TestCase { - from_account: from_account_not_staking, - from_target, - to_target, - retarget_amount: staked_amount, - expected_err: Error::::NotAStakingAccount, - }, - // from and to providers are valid, but zero amount too small - TestCase { - from_account, - from_target, - to_target, - retarget_amount: 0, - expected_err: Error::::StakingAmountBelowMinimum, - }, - // nonzero amount below minimum is still too small - TestCase { - from_account, - from_target, - to_target, - retarget_amount: 9, - expected_err: Error::::StakingAmountBelowMinimum, - }, - // account is staked with from-target, but to-target is not a provider - TestCase { - from_account, - from_target, - to_target: to_target_not_provider, - retarget_amount: staked_amount, - expected_err: Error::::InvalidTarget, - }, - // account doesn't have enough staked to make the transfer - TestCase { - from_account, - from_target, - to_target, - retarget_amount: 999, - expected_err: Error::::InsufficientStakingBalance, - }, - TestCase { - from_account, - from_target, - to_target: from_target, - retarget_amount: 999, - expected_err: Error::::CannotRetargetToSameProvider, - }, - ]; - - for tc in test_cases { - assert_noop!( + struct TestCase { + from_account: u64, + from_target: MessageSourceId, + to_target: MessageSourceId, + retarget_amount: u64, + expected_err: Error, + } + let test_cases: Vec = vec![ + // from is a provider but account is not staking to it + TestCase { + from_account, + from_target: from_target_not_staked, + to_target, + retarget_amount: staked_amount, + expected_err: Error::::StakerTargetRelationshipNotFound, + }, + // from_account is not staking at all. + TestCase { + from_account: from_account_not_staking, + from_target, + to_target, + retarget_amount: staked_amount, + expected_err: Error::::NotAStakingAccount, + }, + // from and to providers are valid, but zero amount too small + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 0, + expected_err: Error::::StakingAmountBelowMinimum, + }, + // nonzero amount below minimum is still too small + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 9, + expected_err: Error::::StakingAmountBelowMinimum, + }, + // account is staked with from-target, but to-target is not a provider + TestCase { + from_account, + from_target, + to_target: to_target_not_provider, + retarget_amount: staked_amount, + expected_err: Error::::InvalidTarget, + }, + // account doesn't have enough staked to make the transfer + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 999, + expected_err: Error::::InsufficientStakingBalance, + }, + TestCase { + from_account, + from_target, + to_target: from_target, + retarget_amount: 999, + expected_err: Error::::CannotRetargetToSameProvider, + }, + ]; + + for tc in test_cases { + assert_noop!( Capacity::change_staking_target( RuntimeOrigin::signed(tc.from_account), tc.from_target, @@ -455,12 +441,12 @@ fn change_staking_target_test_parametric_validity() { ), tc.expected_err ); - } - }); + } + }); } #[test] fn change_staking_target_cannot_switch_staking_type() { - // if you want to switch staking type you must unstake completely and restake regardless of - // whether it is with an existing or new provider. + // if you want to switch staking type you must unstake completely and restake regardless of + // whether it is with an existing or new provider. } diff --git a/pallets/capacity/src/tests/eras_tests.rs b/pallets/capacity/src/tests/eras_tests.rs index 52c9e7712a..ad5ffb0501 100644 --- a/pallets/capacity/src/tests/eras_tests.rs +++ b/pallets/capacity/src/tests/eras_tests.rs @@ -1,19 +1,38 @@ -use super::mock::*; -use crate::{ - tests::testing_utils::{run_to_block, system_run_to_block}, - CurrentEraInfo, RewardEraInfo, -}; +use super::{mock::*, + testing_utils::{run_to_block, system_run_to_block, setup_provider}}; +use crate::{CurrentEraInfo, RewardEraInfo, StakingRewardPool, RewardPoolInfo}; #[test] -fn start_new_era_if_needed() { +fn start_new_era_if_needed_updates_reward_pool_and_era_info() { new_test_ext().execute_with(|| { CurrentEraInfo::::set(RewardEraInfo { era_index: 1, started_at: 0 }); + StakingRewardPool::::insert(1, RewardPoolInfo { + total_staked_token: 10_000, + total_reward_pool: 1_000, + unclaimed_balance: 1_000, + }); + system_run_to_block(8); + + // TODO: Provider boost, after staking updates reward pool info + // let staker = 10_000; + // let provider_msa: MessageSourceId = 1; + // let stake_amount = 600u64; + // setup_provider(&staker, &provider_msa, &stake_amount, ProviderBoost); + system_run_to_block(9); run_to_block(10); let mut current_era_info = CurrentEraInfo::::get(); assert_eq!(current_era_info.era_index, 2u32); assert_eq!(current_era_info.started_at, 10u32); + assert_eq!(StakingRewardPool::::count(), 2); + let current_reward_pool_info = StakingRewardPool::::get(2).unwrap(); + assert_eq!(current_reward_pool_info, RewardPoolInfo { + total_staked_token: 10_000, + total_reward_pool: 1_000, + unclaimed_balance: 1_000, + }); + system_run_to_block(19); run_to_block(20); current_era_info = CurrentEraInfo::::get(); diff --git a/pallets/capacity/src/tests/mock.rs b/pallets/capacity/src/tests/mock.rs index dbf9fcb5ae..517831743c 100644 --- a/pallets/capacity/src/tests/mock.rs +++ b/pallets/capacity/src/tests/mock.rs @@ -15,7 +15,7 @@ use sp_runtime::{ traits::{BlakeTwo256, Convert, IdentityLookup}, AccountId32, DispatchError, Perbill, }; -use sp_std::ops::Mul; +use sp_std::ops::{Mul, Div}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -145,8 +145,8 @@ impl StakingRewardsProvider for TestStakingRewardsProvider { type RewardEra = TestRewardEra; type Hash = Hash; // use what's in common_primitives::node - fn reward_pool_size() -> Result, DispatchError> { - Ok(1000u64) + fn reward_pool_size(total_staked: BalanceOf) -> BalanceOf { + total_staked.div(10u64) } fn staking_reward_total( diff --git a/pallets/capacity/src/tests/rewards_provider_tests.rs b/pallets/capacity/src/tests/rewards_provider_tests.rs index 5f1fe82ff1..65fcd06840 100644 --- a/pallets/capacity/src/tests/rewards_provider_tests.rs +++ b/pallets/capacity/src/tests/rewards_provider_tests.rs @@ -6,6 +6,7 @@ use crate::{ use frame_support::assert_err; use sp_core::H256; +use sp_std::ops::Div; #[test] fn test_staking_reward_total_happy_path() { @@ -63,7 +64,7 @@ fn test_reward_pool_size_happy_path() { unclaimed_balance: 0u64, }, ); - assert_eq!(Ok(tc.expected_reward_pool), Capacity::reward_pool_size()); + assert_eq!(tc.expected_reward_pool, tc.total_staked.div(10u64)); } }) } diff --git a/pallets/capacity/src/tests/testing_utils.rs b/pallets/capacity/src/tests/testing_utils.rs index 3b37d5b654..b5d288f47a 100644 --- a/pallets/capacity/src/tests/testing_utils.rs +++ b/pallets/capacity/src/tests/testing_utils.rs @@ -3,6 +3,7 @@ use frame_support::{assert_ok, traits::Hooks}; #[allow(unused)] use sp_runtime::traits::SignedExtension; +use common_primitives::capacity::StakingType; use crate::{BalanceOf, CapacityDetails, Config, Event}; use common_primitives::msa::MessageSourceId; @@ -63,3 +64,18 @@ pub fn create_capacity_account_and_fund( capacity_details } +pub fn setup_provider(staker: &u64, target: &MessageSourceId, amount: &u64, staking_type: StakingType) { + let provider_name = String::from("Cst-") + target.to_string().as_str(); + register_provider(*target, provider_name); + if amount.gt(&0u64) { + assert_ok!(Capacity::stake( + RuntimeOrigin::signed(staker.clone()), + *target, + *amount, + staking_type.clone() + )); + let target = Capacity::get_target_for(staker, target).unwrap(); + assert_eq!(target.amount, *amount); + assert_eq!(target.staking_type, staking_type); + } +} diff --git a/pallets/capacity/src/tests/unstaking_tests.rs b/pallets/capacity/src/tests/unstaking_tests.rs index f40b4f0851..035e3f1eee 100644 --- a/pallets/capacity/src/tests/unstaking_tests.rs +++ b/pallets/capacity/src/tests/unstaking_tests.rs @@ -12,7 +12,7 @@ use sp_core::bounded::BoundedVec; #[test] fn unstake_happy_path() { new_test_ext().execute_with(|| { - // TODO: ProviderBoost + // TODO: ProviderBoost after unstake affects reward pool info let token_account = 200; let target: MessageSourceId = 1; let staking_amount = 100; diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index e084a9d49b..f2323170e5 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -330,7 +330,7 @@ pub trait StakingRewardsProvider { type Hash; /// Calculate the size of the reward pool using the current economic model - fn reward_pool_size() -> Result, DispatchError>; + fn reward_pool_size(total_staked: BalanceOf) -> BalanceOf; /// Return the total unclaimed reward in token for `accountId` for `from_era` --> `to_era`, inclusive /// Errors: @@ -343,7 +343,7 @@ pub trait StakingRewardsProvider { /// Validate a payout claim for `accountId`, using `proof` and the provided `payload` StakingRewardClaim. /// Returns whether the claim passes validation. Accounts must first pass `payoutEligible` test. - /// Errors: + /// Errors:: /// - NotAStakingAccount /// - MaxUnlockingChunksExceeded /// - All other conditions that would prevent a reward from being claimed return 'false' @@ -360,7 +360,7 @@ pub trait StakingRewardsProvider { /// The information needed to track a Reward Era #[derive( - PartialEq, Eq, Clone, Default, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, + PartialEq, Eq, Clone, Copy, Default, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, )] pub struct RewardEraInfo where From 0f8161f5fbf604c5757971eb569a365b019e839f Mon Sep 17 00:00:00 2001 From: shannonwells Date: Wed, 11 Oct 2023 12:01:58 -0700 Subject: [PATCH 07/11] fetch main version of impl design doc --- ...capacity_staking_rewards_implementation.md | 321 ++++++++++++------ 1 file changed, 221 insertions(+), 100 deletions(-) diff --git a/designdocs/capacity_staking_rewards_implementation.md b/designdocs/capacity_staking_rewards_implementation.md index ac8f762c16..24f8b02af5 100644 --- a/designdocs/capacity_staking_rewards_implementation.md +++ b/designdocs/capacity_staking_rewards_implementation.md @@ -1,4 +1,26 @@ # Capacity Staking Rewards Implementation + +## Overview +Staking Capacity for rewards is a new feature which allows token holders to stake FRQCY and split the staking +rewards with a Provider they choose. The Provider receives a small reward in Capacity +(which is periodically replenished), and the staker receives a periodic return in FRQCY token. +The amount of Capacity that the Provider would receive in such case is a fraction of what they would get from a +`MaximumCapacity` stake. + +The period of Capacity replenishment - the `Epoch` - and the period of token reward - the `RewardEra`- are different. +Epochs much necessarily be much shorter than rewards because Capacity replenishment needs to be multiple times a day to meet the needs of a high traffic network, and to allow Providers the ability to delay transactions to a time of day with lower network activity if necessary. +Reward eras need to be on a much longer scale, such as every two weeks, because there are potentially orders of magnitude more stakers, and calculating rewards is computationally more intensive than updating Capacity balances for the comparatively few Providers. +In addition, this lets the chain to store Reward history for much longer rather than forcing people to have to take steps to claim rewards. + +### Diagram +This illustrates roughly (and not to scale) how Provider Boost staking works. Just like the current staking behavior, now called Maximized staking, The Capacity generated by staking is added to the Provider's Capacity ledger immediately so it can be used right away. The amount staked is locked in Alice's account, preventing transfer. + +Provider Boost token rewards are earned only for token staked for a complete Reward Era. So Alice does not begin earning rewards until Reward Era 5 in the diagram, and this means Alice must wait until Reward Era 6 to claim rewards for Reward Era 5. Unclaimed reward amounts are actually not minted or transferred until they are claimed, and may also not be calculated until then, depending on the economic model. + +This process will be described in more detail in the Economic Model Design Document. + +![Provider boosted staking](https://github.com/LibertyDSNP/frequency/assets/502640/ffb632f2-79c2-4a09-a906-e4de02e4f348) + The proposed feature is a design for staking FRQCY token in exchange for Capacity and/or FRQCY. It is specific to the Frequency Substrate parachain. It consists of enhancements to the capacity pallet, needed traits and their implementations, and needed runtime configuration. @@ -6,44 +28,70 @@ It consists of enhancements to the capacity pallet, needed traits and their impl This does _not_ outline the economic model for Staking Rewards (also known as "Provider Boosting"); it describes the economic model as a black box, i.e. an interface. ## Context and Scope: -The Frequency Transaction Payment system uses Capacity to pay for certain transactions on chain. Accounts that wish to pay with Capacity must: +The Frequency Transaction Payment system allows certain transactions on chain to be paid for with Capacity. Accounts that wish to pay with Capacity must: 1. Have an [MSA](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/accounts.md) 2. Be a [Provider](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/provider_registration.md) (see also [Provider Permissions and Grants](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/provider_permissions.md)) 3. Stake a minimum amount of FRQCY (on mainnet, UNIT on Rococo testnet) token to receive [Capacity](https://github.com/LibertyDSNP/frequency/blob/main/designdocs/capacity.md). # Problem Statement -This document outlines how to implement the Staking for Rewards feature described in [Capacity Staking Rewards Economic Model (TBD)](TBD), without, at this time, regard to what the economic model actually is. +This document outlines how to implement the Staking for Rewards feature described in [Capacity Staking Rewards Economic Model (TBD)](TBD). +It does not give regard to what the economic model actually is, since that is yet to be determined. ## Glossary -1. **FRQCY**: the native token of the Frequency blockchain +1. **FRQCY**: the native token of Frequency, a Substrate parachain in the Polkdaot blockhain ecosystem. 1. **Capacity**: the non-transferrable utility token which can be used only to pay for certain Frequency transactions. 1. **Account**: a Frequency System Account controlled by a private key and addressed by a public key, having at least a minimum balance (currently 0.01 FRQCY). 1. **Stake** (verb): to lock some amount of a token against transfer for a period of time in exchange for some reward. -1. **RewardEra**: the time period (TBD in blocks or Capacity Epochs) that Staking Rewards are based upon. RewardEra is to distinguish it easily from Substrate's staking pallet Era, or the index of said time period. +1. **RewardEra**: the time period (TBD in blocks) that Staking Rewards are based upon. `RewardEra` is to distinguish it easily from Substrate's staking pallet Era, or the index of said time period. 1. **Staking Reward**: a per-RewardEra share of a staking reward pool of FRQCY tokens for a given staking account. -1. **Reward Pool**: a fixed amount of FRQCY that can be minted for rewards each RewardEra and distributed to stakers. +1. **Reward Pool**: a fixed amount of FRQCY that can be minted for rewards each RewardEra and distributed to stakers. 1. **StakingRewardsProvider**: a trait that encapsulates the economic model for staking rewards, providing functionality for calculating the reward pool and staking rewards. ## Staking Token Rewards ### StakingAccountDetails updates -New fields are added. The field `last_rewarded_at` is to keep track of the last time rewards were claimed for this Staking Account. -MaximumCapacity staking accounts MUST always have the value `None` for `last_rewarded_at`. This should be the default value also. -`MaximumCapacity` is also the default value for `staking_type` and should map to 0. -Finally, `stake_change_unlocking`, a BoundedVec is added which tracks the chunks of when a staking account has changed targets for some amount of funds. +New fields are added. The field **`last_rewarded_at`** is to keep track of the last time rewards were claimed for this Staking Account. +MaximumCapacity staking accounts MUST always have the value `None` for `last_rewarded_at`. +Finally, `stake_change_unlocking`, is added, which stores an `UnlockChunk` when a staking account has changed. +targets for some amount of funds. This is to prevent retarget spamming. + +This will be a V2 of this storage and original StakingAccountDetails will need to be migrated. ```rust -pub struct StakingAccountDetails { +pub struct StakingAccountDetailsV2 { pub active: BalanceOf, pub total: BalanceOf, pub unlocking: BoundedVec, T::EpochNumber>, T::MaxUnlockingChunks>, /// The number of the last StakingEra that this account's rewards were claimed. pub last_rewards_claimed_at: Option, // NEW None means never rewarded, Some(RewardEra) means last rewarded RewardEra. - /// What type of staking this account is doing - pub staking_type: StakingType, // NEW /// staking amounts that have been retargeted are prevented from being retargeted again for the /// configured Thawing Period number of blocks. - pub stake_change_unlocking: BoundedVec, EraOf>, T::MaxUnlockingChunks> // NEW + pub stake_change_unlocking: BoundedVec, T::RewardEra>, T::MaxUnlockingChunks>, // NEW +} +``` + +### StakingTargetDetails updates, StakingHistory +A new field, `staking_type` is added to indicate the type of staking the Account holder is doing in relation to this target. +Staking type may be `MaximumCapacity` or `ProviderBoost`. `MaximumCapacity` is the default value for `staking_type` and maps to 0. + +```rust +/// A per-reward-era record for StakingAccount total_staked amount. +pub struct StakingHistory { // NEW + total_staked: Balance, + reward_era: RewardEra, +} + +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct StakingTargetDetails { + /// The total amount of tokens that have been targeted to the MSA. + pub amount: BalanceOf, + /// The total Capacity that an MSA received. + pub capacity: BalanceOf, + /// The type of staking, which determines ultimate capacity per staked token. + pub staking_type: StakingType, // NEW + /// total staked amounts for each past era, up to StakingRewardsPastErasMax eras. + pub staking_history: BoundedVec, T::RewardEra>, T::StakingRewardsPastErasMax>, // NEW } ``` @@ -51,40 +99,49 @@ pub struct StakingAccountDetails { Changes the thaw period to begin at the first block of next RewardEra instead of immediately. ### Changes to extrinsics -```rust -pub fn stake( - origin: OriginFor, - target: MessageSourceId, - amount: BalanceOf, - staking_type: StakingType // NEW -) -> DispatchResult { - /// NEW BEHAVIOR: - // if the account is new, save the new staking type - // if not new and staking type is different, Error::CannotChangeStakingType -} +#### stake +The parameters for the `stake` extrinsic remain the same and the behavior is the same, in that this creates or adds +more token to a staker-target relationship with type `MaximiumCapacity`. +However, if one calls `stake` with a `target` that `origin` already has a staker-target relationsip with, +it is _not_ a `MaximumCapacity` staking type, it will error with `Error::CannotChangeStakingType`. +#### unstake +The unstake parameters are the same, and unstake behavior is the same for `MaximumCapacity` as before, however +for a `ProviderBoost` staker-target relationship, the behavior must be different. While it's not feasible to +store either `reward_pool` history or individual staking reward history indefinitely, it still may be lengthy +enough that having to calculate _all_ unclaimed rewards for what could be numerous accounts in one block +could make a block heavier than desired. Therefore there must be a limit limit on how many eras +one can claim rewards for. This value will likely be a pallet constant. The logic would be: + + * If a ProviderBoost stake is `payout_eligible`, + * check whether their last payout era is recent enough to pay out all rewards at once. + * if so, first pay out all rewards and then continue with rest of unstaking code as is + * if not, emit error `MustFirstClaimRewards`, `UnclaimedRewardsOverTooManyEras` or something like that. + Don't use `EraOutOfRange` because it will overload the meaning of that error; needs to be something more specific. + * If not payout eligible, + * check whether the last payout era is the current one. + * if so, all rewards have been claimed, so continue with rest of unstaking code as is, + * if not, it means they have too many unlocking chunks so they'll have to wait. - the unstaking code + will catch this anyway and emit `MaxUnlockingChunksExceeded` + +```rust pub fn unstake( origin: OriginFor, target: MessageSourceId, requested_amount: BalanceOf, -) -> DispatchResult { - // NEW BEHAVIOR: - // If StakingType is RewardsType - // If payout_eligible, - // check whether their last payout era is recent enough to pay out all rewards at once. - // if so, first pay out all rewards and then continue with rest of unstaking code as is - // if not, emit error "MustFirstClaimUnclaimedRewards", "UnclaimedRewardsOverTooManyEras" or something like that - // If not payout eligible, - // check whether the last payout era is the current one. - // if so, all rewards have been claimed, so continue with rest of unstaking code as is, - // - // otherwise, they have too many unlocking chunks so they'll have to wait. - the unstaking code - // will catch this anyway and emit `MaxUnlockingChunksExceeded` -} +) -> DispatchResult {} + ``` ### NEW: StakingRewardsProvider - Economic Model trait -This one is most likely to change, however there are certain functions that will definitely be needed. -The struct and method for claiming rewards is probably going to change, but the rewards system will still need to know the `reward_pool_size` and the `staking_reward_total` for a given staker. +This one is not yet determined, however there are certain functions that will definitely be needed. +The rewards system will still need to know the `reward_pool_size`. + +The struct and method for claiming rewards is probably going to change. +The `staking_reward_total` for a given staker may not be calculable by the node, depending on the complexity of the +economic rewards model. +It's possible that it would be calculated via some app with access to the staker's wallet, and submitted as a proof +with a payload. +In that case the `validate_staking_reward_claim` is more likely to be part of the trait. ```rust use std::hash::Hash; @@ -108,6 +165,7 @@ pub trait StakingRewardsProvider { /// Return the total unclaimed reward in token for `account_id` for `fromEra` --> `toEra`, inclusive /// Errors: /// - EraOutOfRange when fromEra or toEra are prior to the history retention limit, or greater than the current RewardEra. + /// May not be possible depending on economic model complexity. fn staking_reward_total(account_id: T::AccountId, fromEra: T::RewardEra, toEra: T::RewardEra); /// Validate a payout claim for `account_id`, using `proof` and the provided `payload` StakingRewardClaim. @@ -117,17 +175,6 @@ pub trait StakingRewardsProvider { } ``` -### NEW: StakingType enum -```rust -pub enum StakingType { - /// Staking account targets Providers for capacity only, no token reward - MaximizedCapacity, - /// Staking account targets Providers and splits reward between capacity to the Provider - /// and token for the account holder - Rewards, -} -``` - ### NEW: Config items ```rust pub trait Config: frame_system::Config { @@ -149,24 +196,35 @@ pub trait Config: frame_system::Config { type EraLength: Get; /// The maximum number of eras over which one can claim rewards type StakingRewardsPastErasMax: Get; - + /// The trait providing the ProviderBoost economic model calculations and values type RewardsProvider: StakingRewardsProvider; }; ``` -### NEW: RewardPoolInfo -This is the necessary information about the reward pool for a given Reward Era and how it's stored. +### NEW: RewardPoolInfo, RewardPoolHistory +Information about the reward pool for a given Reward Era and how it's stored. The size of this pool is limited to +`StakingRewardsPastErasMax` but is stored as a CountedStorageMap instead of a BoundedVec for performance reasons: +* claiming rewards for the entire history will be unlikely to be allowed. Iterating over a much smaller range is more performant +* Fetching/writing the entire history every block could affect block times. Instead, once per block, retrieve the latest record, delete the earliest record and insert a new one ```rust -pub struct RewardPoolInfo { +pub struct RewardPoolInfo { + /// the total staked for rewards in the associated RewardEra + pub total_staked_token: Balance, + /// the reward pool for this era + pub total_reward_pool: Balance, + /// the remaining rewards balance to be claimed + pub unclaimed_balance: Balance, } + /// Reward Pool history #[pallet::storage] #[pallet::getter(fn get_reward_pool_for_era)] -pub type StakingRewardPool = ; +pub type StakingRewardPool = ; ``` ### NEW: CurrentEra, RewardEraInfo Incremented, like CurrentEpoch, tracks the current RewardEra number and the block when it started. +Storage is whitelisted because it's accessed every block and would improperly adversely impact all benchmarks. ```rust #[pallet::storage] #[pallet::whitelist_storage] @@ -192,60 +250,105 @@ pub enum Error { EraOutOfRange, /// Rewards were already paid out for the specified Era range IneligibleForPayoutInEraRange, + /// Attempted to retarget but from and to Provider MSA Ids were the same + CannotRetargetToSameProvider, + /// Rewards were already paid out this era + AlreadyClaimedRewardsThisEra, } ``` ### NEW Extrinsics -1. *claim_staking_reward*, first version - a. `claim_staking_reward(origin,proof,payload)` - ```rust - /// TBD whether this is the form for claiming rewards. This could be the form if calculations are - /// done off chain and submitted for validation. +This is the most undecided portion of this design and depends strongly on the chosen economic model for Provider Boosting. +There are generally two forms that claiming a staking reward could take, and this depends on whether it's possible to +calculate rewards on chain at all. + +Regardless, on success, the claimed rewards are minted and transferred as locked token to the origin, with the existing +unstaking thaw period for withdrawal (which simply unlocks thawed token amounts as before). +There is no chunk added; instead the existing unstaking thaw period is applied to last_rewards_claimed_at in StakingAccountDetails. + +Forcing stakers to wait a thaw period for every claim is an incentive to claim rewards sooner than later, leveling out +possible inflationary effects and helping prevent unclaimed rewards from expiring. +The thaw period must be short enough for all rewards to be claimed before rewards history would end. +Therefore, it's possible that a complete separate reward claim thaw period would need to be used. + +For all forms of claim_staking_reward, the event `StakingRewardClaimed` is emitted with the parameters of the extrinsic. + +#### provider_boost(origin, target, amount) +Like `stake`, except this extrinsic creates or adds staked token to a `ProviderBoost` type staker-target relationship. +In the case of an increase in stake, `staking_type` MUST be a `ProviderBoost` type, or else it will error with `Error::CannotChangeStakingType`. +```rust +pub fn provider_boost( + origin: OriginFor, + target: MessageSourceId, + amount: BalanceOf, +) -> DispatchResult {} +``` + +#### 1. claim_staking_reward(origin, from_era, to_era), simple economic model +In the case of a simple economic model such as a fixed rate return, reward calculations may be done on chain - +within discussed limits. +```rust +/// Claim staking rewards from `from_era` to `to_era`, inclusive. +/// from_era: if None, since last_reward_claimed_at +/// to_era: if None, to CurrentEra - 1 +/// Errors: +/// - NotAStakingAccount: if Origin does not own the StakingRewardDetails in the claim. +/// - IneligibleForPayoutInEraRange: if rewards were already paid out in the provided RewardEra range +/// - EraOutOfRange: +/// - if `from_era` is earlier than history storage +/// - if `to_era` is >= current era +/// - if `to_era` - `from_era` > StakingRewardsPastErasMax +#[pallet::call_index(n)] +pub fn claim_staking_reward( + origin: OriginFor, + from_era: Option, + to_era: Option +); +``` + +#### 2. claim_staking_reward(origin,proof,payload) +TBD whether this is the form for claiming rewards. +This could be the form if calculations are done off chain and submitted for validation. + +```rust /// Validates the reward claim. If validated, mints token and transfers to Origin. /// Errors: /// - NotAStakingAccount: if Origin does not own the StakingRewardDetails in the claim. /// - StakingRewardClaimInvalid: if validation of calculation fails /// - IneligibleForPayoutInEraRange: if rewards were already paid out in the provided RewardEra range - /// - EraOutOfRange: if one or both of the StakingRewardClaim eras are invalid - /// `proof` - the Merkle proof for the reward claim + /// - EraOutOfRange: + /// - if `from_era` is earlier than history storage + /// - if `to_era` is >= current era + /// - if `to_era` - `from_era` > StakingRewardsPastErasMax #[pallet::call_index(n)] pub fn claim_staking_reward( origin: OriginFor, + /// `proof` - the Merkle proof for the reward claim proof: Hash, + /// The staking reward claim payload for which the proof was generated payload: StakingRewardClaim ); - ``` - b. *claim_staking_reward*, alternate version - ```rust - /// An alternative, depending on staking reward economic model. This could be the form if calculations are done on chain. - /// from_era: if None, since last_reward_claimed_at - /// to_era: if None, to CurrentEra - 1 - /// Errors: - /// - NotAStakingAccount: if Origin does not own the StakingRewardDetails in the claim. - /// - IneligibleForPayoutInEraRange: if rewards were already paid out in the provided RewardEra range - /// - EraOutOfRange: if one or both of the eras specified are invalid - #[pallet::call_index(n)] - pub fn claim_staking_reward( - origin: OriginFor, - from_era: Option, - to_era: Option - ); - ``` - Both emit events `StakingRewardClaimed` with the parameters of the extrinsic. - -2. **change_staking_target(origin, from, to, amount)** +``` +#### 3. change_staking_target(origin, from, to, amount) Changes a staking account detail's target MSA Id to a new one by `amount` Rules for this are similar to unstaking; if `amount` would leave less than the minimum staking amount for the `from` target, the entire amount is retargeted. -No more than T::MaxUnlockingChunks staking amounts may be retargeted within this Thawing Period. +No more than `T::MaxUnlockingChunks` staking amounts may be retargeted within this Thawing Period. Each call creates one chunk. Emits a `StakingTargetChanged` event with the parameters of the extrinsic. - ```rust -/// Errors: -/// - MaxUnlockingChunksExceeded if 'from' target staking amount is still thawing in the staking unlock chunks (either type) -/// - StakerTargetRelationshipNotFound` if `from` is not a staking target for Origin. This also covers when account's MSA is not staking anything at all or account has no MSA -/// - StakingAmountBelowMinimum if amount to retarget is below the minimum staking amount. -/// - InsufficientStakingBalance if amount to retarget exceeds what the staker has targeted to the `from` MSA Id. -/// - InvalidTarget if `to` is not a Registered Provider. +/// Sets the target of the staking capacity to a new target. +/// This adds a chunk to `StakingAccountDetails.stake_change_unlocking chunks`, up to `T::MaxUnlockingChunks`. +/// The staked amount and Capacity generated by `amount` originally targeted to the `from` MSA Id is reassigned to the `to` MSA Id. +/// Does not affect unstaking process or additional stake amounts. +/// Changing a staking target to a Provider when Origin has nothing staked them will retain the staking type. +/// Changing a staking target to a Provider when Origin has any amount staked to them will error if the staking types are not the same. +/// ### Errors +/// - [`Error::NotAStakingAccount`] if origin does not have a staking account +/// - [`Error::MaxUnlockingChunksExceeded`] if `stake_change_unlocking_chunks` == `T::MaxUnlockingChunks` +/// - [`Error::StakerTargetRelationshipNotFound`] if `from` is not a target for Origin's staking account. +/// - [`Error::StakingAmountBelowMinimum`] if `amount` to retarget is below the minimum staking amount. +/// - [`Error::InsufficientStakingBalance`] if `amount` to retarget exceeds what the staker has targeted to `from` MSA Id. +/// - [`Error::InvalidTarget`] if `to` does not belong to a registered Provider. +/// - [`Error::CannotChangeStakingType`] if origin already has funds staked for `to` and the staking type for `from` is different. #[pallet:call_index(n+1)] // n = current call index in the pallet pub fn change_staking_target( origin: OriginFor, @@ -256,19 +359,37 @@ pub fn change_staking_target( ``` ### NEW: Capacity pallet helper function +#### payout_eligible +Returns whether `account_id` can claim a reward at all. +This function will return false if there is no staker-target relationship. +Staking accounts may claim rewards: +* ONCE per RewardEra, +* Only for funds staked for a complete RewardEra, i.e. the balance at the end of the Era, +* Must wait for the thaw period to claim rewards again (see `last_rewards_claimed_at`) ```rust -/// Return whether `account_id` can claim a reward. Staking accounts may not claim a reward more than once -/// per RewardEra, may not claim rewards before a complete RewardEra has been staked, and may not claim more rewards past -/// the number of `MaxUnlockingChunks`. -/// Errors: -/// NotAStakingAccount if account_id has no StakingAccountDetails in storage. fn payout_eligible(account_id: AccountIdOf) -> bool; ``` -### NEW RPC +### NEW RPCS There are no custom RPCs for the Capacity pallet, so that work will need to be done first. + +The form of this will depend on whether the rewards calculation for an individual account is done by the node or externally +with a submitted proof. If externally, then unclaimed rewards would not include an earned amount. + ```rust -/// RPC access to the pallet function by the same name -pub fn payout_eligible(account_id: AccountId) -> bool; -``` +pub struct UnclaimedRewardInfo { + /// The Reward Era for which this reward was earned + reward_era: RewardEra, + /// An ISO8701 string, UTC, estimated using current block time, and the number of blocks between + /// the current block and the block when this era's RewardPoolInfo would be removed from StakingRewardPool history + expires_at: string, + /// The amount staked in this era + staked_amount: BalanceOf, + /// The amount in token of the reward (only if it can be calculated using only on chain data) + earned_amount: BalanceOf +} +/// Check what unclaimed rewards origin has and how long they have left to claim them +/// If no unclaimed rewards, returns empty list. +fn check_for_unclaimed_rewards(origin: OriginFor) -> Vec; +``` From 101288c604f5f7ad52150ecaa4a9e58cff111ec1 Mon Sep 17 00:00:00 2001 From: shannonwells Date: Wed, 11 Oct 2023 14:48:41 -0700 Subject: [PATCH 08/11] tests passing --- pallets/capacity/src/lib.rs | 36 +- .../src/tests/change_staking_target_tests.rs | 629 +++++++++--------- pallets/capacity/src/tests/mock.rs | 2 +- pallets/capacity/src/tests/testing_utils.rs | 9 +- pallets/capacity/src/types.rs | 12 +- 5 files changed, 355 insertions(+), 333 deletions(-) diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index 2fc5ebc8dd..2d5b093fee 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -48,6 +48,7 @@ rustdoc::invalid_codeblock_attributes, missing_docs )] +use sp_std::ops::{Add, Mul}; use frame_support::{ dispatch::DispatchResult, @@ -60,7 +61,6 @@ use sp_runtime::{ traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero}, ArithmeticError, DispatchError, Perbill, }; -use sp_std::ops::Mul; pub use common_primitives::{ capacity::{Nontransferable, Replenishable, TargetValidator}, @@ -778,31 +778,34 @@ impl Pallet { fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight { let current_era_info: RewardEraInfo = Self::get_current_era(); // 1r - if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { // 1r + if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { + // 1r let new_era_info = RewardEraInfo { era_index: current_era_info.era_index.saturating_add(One::one()), started_at: current_block, }; - let current_reward_pool = Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r - let past_eras_max = T::StakingRewardsPastErasMax::get(); + let current_reward_pool_info = + Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r + let past_eras_max = T::StakingRewardsPastErasMax::get(); let entries: u32 = StakingRewardPool::::count(); - if past_eras_max.eq(&entries.into()) { // 2r + if past_eras_max.eq(&entries.into()) { + // 2r let current_era = Self::get_current_era().era_index; - let earliest_era = current_era.saturating_sub(past_eras_max); + let earliest_era = current_era.saturating_sub(past_eras_max).add(One::one()); StakingRewardPool::::remove(earliest_era); // 1w } - CurrentEraInfo::::set(new_era_info); // 1w + CurrentEraInfo::::set(new_era_info); // 1w // let msa_handle = T::HandleProvider::get_handle_for_msa(msa_id); let total_reward_pool = - T::RewardsProvider::reward_pool_size(current_reward_pool.total_staked_token.clone()); + T::RewardsProvider::reward_pool_size(current_reward_pool_info.total_staked_token); let new_reward_pool = RewardPoolInfo { - total_staked_token: current_reward_pool.total_staked_token, + total_staked_token: current_reward_pool_info.total_staked_token, total_reward_pool, - unclaimed_balance: total_reward_pool.clone(), + unclaimed_balance: total_reward_pool, }; - StakingRewardPool::::insert(new_era_info.era_index.clone(), new_reward_pool); // 1w + StakingRewardPool::::insert(new_era_info.era_index, new_reward_pool); // 1w T::WeightInfo::on_initialize() .saturating_add(T::DbWeight::get().reads(5)) @@ -849,10 +852,13 @@ impl Pallet { let mut to_msa_target = Self::get_target_for(staker, to_msa).unwrap_or_default(); - if to_msa_target.amount.is_zero() { + if to_msa_target.amount.is_zero() { to_msa_target.staking_type = staking_type.clone(); } else { - ensure!(to_msa_target.staking_type.ne(staking_type), Error::::CannotChangeStakingType); + ensure!( + to_msa_target.staking_type.eq(staking_type), + Error::::CannotChangeStakingType + ); } to_msa_target .deposit(*amount, capacity_withdrawn) @@ -966,9 +972,7 @@ impl StakingRewardsProvider for Pallet { } // For now reward pool size is set to 10% of total staked token - total_staked - .checked_div(&BalanceOf::::from(10u8)) - .unwrap_or_default() + total_staked.checked_div(&BalanceOf::::from(10u8)).unwrap_or_default() } // Performs range checks plus a reward calculation based on economic model for the era range diff --git a/pallets/capacity/src/tests/change_staking_target_tests.rs b/pallets/capacity/src/tests/change_staking_target_tests.rs index e52ee039de..8ca36cc494 100644 --- a/pallets/capacity/src/tests/change_staking_target_tests.rs +++ b/pallets/capacity/src/tests/change_staking_target_tests.rs @@ -1,14 +1,17 @@ -use super::{mock::*, testing_utils::{setup_provider, staking_events} }; +use super::{ + mock::*, + testing_utils::{setup_provider, staking_events}, +}; use crate::{ - BalanceOf, CapacityDetails, Config, CurrentEraInfo, Error, Event, RewardEraInfo, - StakingAccountDetails, StakingAccountLedger, StakingTargetDetails, + BalanceOf, CapacityDetails, Config, CurrentEraInfo, Error, Event, RewardEraInfo, + StakingAccountDetails, StakingAccountLedger, StakingTargetDetails, }; use common_primitives::{ - capacity::{ - StakingType, - StakingType::{MaximumCapacity, ProviderBoost}, - }, - msa::MessageSourceId, + capacity::{ + StakingType, + StakingType::{MaximumCapacity, ProviderBoost}, + }, + msa::MessageSourceId, }; use frame_support::{assert_noop, assert_ok, traits::Get}; @@ -17,282 +20,282 @@ type TestCapacityDetails = CapacityDetails, u32>; type TestTargetDetails = StakingTargetDetails; fn assert_capacity_details( - msa_id: MessageSourceId, - remaining_capacity: u64, - total_tokens_staked: u64, - total_capacity_issued: u64, + msa_id: MessageSourceId, + remaining_capacity: u64, + total_tokens_staked: u64, + total_capacity_issued: u64, ) { - let expected_from_details: TestCapacityDetails = CapacityDetails { - remaining_capacity, - total_tokens_staked, - total_capacity_issued, - last_replenished_epoch: 0, - }; - let from_capacity_details: TestCapacityDetails = Capacity::get_capacity_for(msa_id).unwrap(); - assert_eq!(from_capacity_details, expected_from_details); + let expected_from_details: TestCapacityDetails = CapacityDetails { + remaining_capacity, + total_tokens_staked, + total_capacity_issued, + last_replenished_epoch: 0, + }; + let from_capacity_details: TestCapacityDetails = Capacity::get_capacity_for(msa_id).unwrap(); + assert_eq!(from_capacity_details, expected_from_details); } fn assert_target_details( - staker: u64, - msa_id: MessageSourceId, - amount: u64, - capacity: u64, - staking_type: StakingType, + staker: u64, + msa_id: MessageSourceId, + amount: u64, + capacity: u64, + staking_type: StakingType, ) { - let expected_from_target_details: TestTargetDetails = - StakingTargetDetails { amount, capacity, staking_type }; - let from_target_details = Capacity::get_target_for(staker, msa_id).unwrap(); - assert_eq!(from_target_details, expected_from_target_details); + let expected_from_target_details: TestTargetDetails = + StakingTargetDetails { amount, capacity, staking_type }; + let from_target_details = Capacity::get_target_for(staker, msa_id).unwrap(); + assert_eq!(from_target_details, expected_from_target_details); } #[test] fn do_retarget_happy_path() { - new_test_ext().execute_with(|| { - let staker = 10_000; - let from_msa: MessageSourceId = 1; - let from_amount = 600u64; - let to_amount = 300u64; - let to_msa: MessageSourceId = 2; - let staking_type = ProviderBoost; - setup_provider(&staker, &from_msa, &from_amount, staking_type.clone()); - setup_provider(&staker, &to_msa, &to_amount, staking_type.clone()); + new_test_ext().execute_with(|| { + let staker = 10_000; + let from_msa: MessageSourceId = 1; + let from_amount = 600u64; + let to_amount = 300u64; + let to_msa: MessageSourceId = 2; + let staking_type = ProviderBoost; + setup_provider(&staker, &from_msa, &from_amount, staking_type.clone()); + setup_provider(&staker, &to_msa, &to_amount, staking_type.clone()); - // retarget half the stake to to_msa - assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &to_amount, &staking_type)); + // retarget half the stake to to_msa + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &to_amount, &staking_type)); - // expect from stake amounts to be halved - assert_capacity_details(from_msa, 1, 300, 1); + // expect from stake amounts to be halved + assert_capacity_details(from_msa, 1, 300, 1); - // expect to stake amounts to be increased by the retarget amount - assert_capacity_details(to_msa, 3, 600, 3); + // expect to stake amounts to be increased by the retarget amount + assert_capacity_details(to_msa, 3, 600, 3); - assert_target_details(staker, from_msa, 300, 1, staking_type.clone()); + assert_target_details(staker, from_msa, 300, 1, staking_type.clone()); - assert_target_details(staker, to_msa, 600, 3, staking_type.clone()); - }) + assert_target_details(staker, to_msa, 600, 3, staking_type.clone()); + }) } #[test] fn do_retarget_flip_flop() { - new_test_ext().execute_with(|| { - let staker = 10_000; - let from_msa: MessageSourceId = 1; - let from_amount = 600u64; - let to_amount = 300u64; - let to_msa: MessageSourceId = 2; - setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); - - for i in 0..4 { - if i % 2 == 0 { - assert_ok!(Capacity::do_retarget( + new_test_ext().execute_with(|| { + let staker = 10_000; + let from_msa: MessageSourceId = 1; + let from_amount = 600u64; + let to_amount = 300u64; + let to_msa: MessageSourceId = 2; + setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); + + for i in 0..4 { + if i % 2 == 0 { + assert_ok!(Capacity::do_retarget( &staker, &from_msa, &to_msa, &to_amount, &ProviderBoost )); - } else { - assert_ok!(Capacity::do_retarget( + } else { + assert_ok!(Capacity::do_retarget( &staker, &to_msa, &from_msa, &to_amount, &ProviderBoost )); - } - } - assert_capacity_details(from_msa, 3, 600, 3); - assert_capacity_details(to_msa, 1, 300, 1); - }) + } + } + assert_capacity_details(from_msa, 3, 600, 3); + assert_capacity_details(to_msa, 1, 300, 1); + }) } #[test] // check that no capacity is minted or burned just by retargeting. fn check_retarget_rounding_errors() { - new_test_ext().execute_with(|| { - let staker = 10_000; - let from_msa: MessageSourceId = 1; - let from_amount = 666u64; - let to_amount = 301u64; - let to_msa: MessageSourceId = 2; - - setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); - assert_capacity_details(from_msa, 3, 666, 3); - assert_capacity_details(to_msa, 1, 301, 1); - // 666+301= 967, 3+1=4 - - assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &301u64, &ProviderBoost)); - assert_capacity_details(to_msa, 3, 602, 3); - assert_capacity_details(from_msa, 1, 365, 1); - // 602+365 = 967, 3+1 = 4 - - assert_ok!(Capacity::do_retarget(&staker, &to_msa, &from_msa, &151u64, &ProviderBoost)); - assert_capacity_details(to_msa, 2, 451, 2); - assert_capacity_details(from_msa, 2, 516, 2); - // 451+516 = 967, 2+2 = 4 - }) + new_test_ext().execute_with(|| { + let staker = 10_000; + let from_msa: MessageSourceId = 1; + let from_amount = 666u64; + let to_amount = 301u64; + let to_msa: MessageSourceId = 2; + + setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); + assert_capacity_details(from_msa, 3, 666, 3); + assert_capacity_details(to_msa, 1, 301, 1); + // 666+301= 967, 3+1=4 + + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &301u64, &ProviderBoost)); + assert_capacity_details(to_msa, 3, 602, 3); + assert_capacity_details(from_msa, 1, 365, 1); + // 602+365 = 967, 3+1 = 4 + + assert_ok!(Capacity::do_retarget(&staker, &to_msa, &from_msa, &151u64, &ProviderBoost)); + assert_capacity_details(to_msa, 2, 451, 2); + assert_capacity_details(from_msa, 2, 516, 2); + // 451+516 = 967, 2+2 = 4 + }) } fn assert_total_capacity(msas: Vec, total: u64) { - let sum = msas - .into_iter() - .map(|a| { - let capacity_details: TestCapacityDetails = Capacity::get_capacity_for(a).unwrap(); - capacity_details.total_capacity_issued - }) - .fold(0, |a, b| a + b); - assert_eq!(total, sum); + let sum = msas + .into_iter() + .map(|a| { + let capacity_details: TestCapacityDetails = Capacity::get_capacity_for(a).unwrap(); + capacity_details.total_capacity_issued + }) + .fold(0, |a, b| a + b); + assert_eq!(total, sum); } #[test] fn check_retarget_multiple_stakers() { - new_test_ext().execute_with(|| { - let staker_10k = 10_000; - let staker_600 = 600u64; - let staker_500 = 500u64; - let staker_400 = 400u64; - - let from_msa: MessageSourceId = 1; - let to_msa: MessageSourceId = 2; - let amt1 = 192u64; - let amt2 = 313u64; - - setup_provider(&staker_10k, &from_msa, &647u64, ProviderBoost); - setup_provider(&staker_500, &to_msa, &293u64, ProviderBoost); - assert_ok!(Capacity::stake( + new_test_ext().execute_with(|| { + let staker_10k = 10_000; + let staker_600 = 600u64; + let staker_500 = 500u64; + let staker_400 = 400u64; + + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + let amt1 = 192u64; + let amt2 = 313u64; + + setup_provider(&staker_10k, &from_msa, &647u64, ProviderBoost); + setup_provider(&staker_500, &to_msa, &293u64, ProviderBoost); + assert_ok!(Capacity::stake( RuntimeOrigin::signed(staker_600.clone()), from_msa, 479u64, MaximumCapacity )); - assert_ok!(Capacity::stake( + assert_ok!(Capacity::stake( RuntimeOrigin::signed(staker_400.clone()), to_msa, 211u64, MaximumCapacity )); - // 647 * .1 * .05 = 3 (rounded down) - // 293 * .1 * .05 = 1 (rounded down) - // 479 * .1 = 48 (rounded up) - // 211 * .1 = 21 (rounded down) - // total capacity should be 73 - assert_total_capacity(vec![from_msa, to_msa], 73); - - assert_ok!(Capacity::do_retarget(&staker_10k, &from_msa, &to_msa, &amt2, &ProviderBoost)); - assert_ok!(Capacity::do_retarget(&staker_600, &from_msa, &to_msa, &amt1, &MaximumCapacity)); - assert_ok!(Capacity::do_retarget(&staker_500, &to_msa, &from_msa, &amt1, &ProviderBoost)); - assert_ok!(Capacity::do_retarget(&staker_400, &to_msa, &from_msa, &amt1, &MaximumCapacity)); - assert_total_capacity(vec![from_msa, to_msa], 73); - }) + // 647 * .1 * .05 = 3 (rounded down) + // 293 * .1 * .05 = 1 (rounded down) + // 479 * .1 = 48 (rounded up) + // 211 * .1 = 21 (rounded down) + // total capacity should be 73 + assert_total_capacity(vec![from_msa, to_msa], 73); + + assert_ok!(Capacity::do_retarget(&staker_10k, &from_msa, &to_msa, &amt2, &ProviderBoost)); + assert_ok!(Capacity::do_retarget(&staker_600, &from_msa, &to_msa, &amt1, &MaximumCapacity)); + assert_ok!(Capacity::do_retarget(&staker_500, &to_msa, &from_msa, &amt1, &ProviderBoost)); + assert_ok!(Capacity::do_retarget(&staker_400, &to_msa, &from_msa, &amt1, &MaximumCapacity)); + assert_total_capacity(vec![from_msa, to_msa], 73); + }) } #[test] fn do_retarget_deletes_staking_target_details_if_zero_balance() { - new_test_ext().execute_with(|| { - let staker = 200u64; - let from_msa: MessageSourceId = 1; - let to_msa: MessageSourceId = 2; - let amount = 10u64; - setup_provider(&staker, &from_msa, &amount, MaximumCapacity); - setup_provider(&staker, &to_msa, &amount, MaximumCapacity); - - // stake additional to provider from another Msa, doesn't matter which type. - // total staked to from_msa is now 22u64. - assert_ok!(Capacity::stake( + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + let amount = 10u64; + setup_provider(&staker, &from_msa, &amount, MaximumCapacity); + setup_provider(&staker, &to_msa, &amount, MaximumCapacity); + + // stake additional to provider from another Msa, doesn't matter which type. + // total staked to from_msa is now 22u64. + assert_ok!(Capacity::stake( RuntimeOrigin::signed(300u64), from_msa, 12u64, MaximumCapacity )); - assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &amount, &MaximumCapacity)); - - let expected_from_details: TestCapacityDetails = CapacityDetails { - remaining_capacity: 1, - total_tokens_staked: 12, - total_capacity_issued: 1, - last_replenished_epoch: 0, - }; - let from_capacity_details: TestCapacityDetails = - Capacity::get_capacity_for(from_msa).unwrap(); - assert_eq!(from_capacity_details, expected_from_details); - - let expected_to_details: TestCapacityDetails = CapacityDetails { - remaining_capacity: 2, - total_tokens_staked: 2 * amount, - total_capacity_issued: 2, - last_replenished_epoch: 0, - }; - - let to_capacity_details = Capacity::get_capacity_for(to_msa).unwrap(); - assert_eq!(to_capacity_details, expected_to_details); - - assert!(Capacity::get_target_for(staker, from_msa).is_none()); - - let expected_to_target_details: TestTargetDetails = StakingTargetDetails { - amount: 2 * amount, - capacity: 2, - staking_type: StakingType::MaximumCapacity, - }; - let to_target_details = Capacity::get_target_for(staker, to_msa).unwrap(); - assert_eq!(to_target_details, expected_to_target_details); - - assert!(Capacity::get_target_for(staker, from_msa).is_none()); - }) + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &amount, &MaximumCapacity)); + + let expected_from_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 1, + total_tokens_staked: 12, + total_capacity_issued: 1, + last_replenished_epoch: 0, + }; + let from_capacity_details: TestCapacityDetails = + Capacity::get_capacity_for(from_msa).unwrap(); + assert_eq!(from_capacity_details, expected_from_details); + + let expected_to_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 2, + total_tokens_staked: 2 * amount, + total_capacity_issued: 2, + last_replenished_epoch: 0, + }; + + let to_capacity_details = Capacity::get_capacity_for(to_msa).unwrap(); + assert_eq!(to_capacity_details, expected_to_details); + + assert!(Capacity::get_target_for(staker, from_msa).is_none()); + + let expected_to_target_details: TestTargetDetails = StakingTargetDetails { + amount: 2 * amount, + capacity: 2, + staking_type: StakingType::MaximumCapacity, + }; + let to_target_details = Capacity::get_target_for(staker, to_msa).unwrap(); + assert_eq!(to_target_details, expected_to_target_details); + + assert!(Capacity::get_target_for(staker, from_msa).is_none()); + }) } #[test] fn change_staking_starget_emits_event_on_success() { - new_test_ext().execute_with(|| { - let staker = 200u64; - let from_msa: MessageSourceId = 1; - let from_amount = 20u64; - let to_amount = from_amount / 2; - let to_msa: MessageSourceId = 2; - setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); - - assert_ok!(Capacity::change_staking_target( + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let from_amount = 20u64; + let to_amount = from_amount / 2; + let to_msa: MessageSourceId = 2; + setup_provider(&staker, &from_msa, &from_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &to_amount, ProviderBoost); + + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staker), from_msa, to_msa, to_amount )); - let events = staking_events(); + let events = staking_events(); - assert_eq!( - events.last().unwrap(), - &Event::StakingTargetChanged { account: staker, from_msa, to_msa, amount: to_amount } - ); - }) + assert_eq!( + events.last().unwrap(), + &Event::StakingTargetChanged { account: staker, from_msa, to_msa, amount: to_amount } + ); + }) } #[test] fn change_staking_target_errors_if_too_many_changes_before_thaw() { - new_test_ext().execute_with(|| { - let staker = 200u64; - let from_msa: MessageSourceId = 1; - let to_msa: MessageSourceId = 2; - - let max_chunks: u32 = ::MaxUnlockingChunks::get(); - let staking_amount = ((max_chunks + 2u32) * 10u32) as u64; - setup_provider(&staker, &from_msa, &staking_amount, ProviderBoost); - setup_provider(&staker, &to_msa, &10u64, ProviderBoost); - - let retarget_amount = 10u64; - for _i in 0..(max_chunks) { - assert_ok!(Capacity::change_staking_target( + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + + let max_chunks: u32 = ::MaxUnlockingChunks::get(); + let staking_amount = ((max_chunks + 2u32) * 10u32) as u64; + setup_provider(&staker, &from_msa, &staking_amount, ProviderBoost); + setup_provider(&staker, &to_msa, &10u64, ProviderBoost); + + let retarget_amount = 10u64; + for _i in 0..(max_chunks) { + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staker), from_msa, to_msa, retarget_amount )); - } + } - assert_noop!( + assert_noop!( Capacity::change_staking_target( RuntimeOrigin::signed(staker), from_msa, @@ -301,138 +304,138 @@ fn change_staking_target_errors_if_too_many_changes_before_thaw() { ), Error::::MaxUnlockingChunksExceeded ); - }); + }); } #[test] fn change_staking_target_garbage_collects_thawed_chunks() { - new_test_ext().execute_with(|| { - let staked_amount = 50u64; - let staking_account = 200u64; - let from_target: MessageSourceId = 3; - let to_target: MessageSourceId = 4; - setup_provider(&staking_account, &from_target, &staked_amount, ProviderBoost); - setup_provider(&staking_account, &to_target, &staked_amount, ProviderBoost); - - CurrentEraInfo::::set(RewardEraInfo { era_index: 20, started_at: 100 }); - let max_chunks = ::MaxUnlockingChunks::get(); - for _i in 0..max_chunks { - assert_ok!(Capacity::change_staking_target( + new_test_ext().execute_with(|| { + let staked_amount = 50u64; + let staking_account = 200u64; + let from_target: MessageSourceId = 3; + let to_target: MessageSourceId = 4; + setup_provider(&staking_account, &from_target, &staked_amount, ProviderBoost); + setup_provider(&staking_account, &to_target, &staked_amount, ProviderBoost); + + CurrentEraInfo::::set(RewardEraInfo { era_index: 20, started_at: 100 }); + let max_chunks = ::MaxUnlockingChunks::get(); + for _i in 0..max_chunks { + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staking_account), from_target, to_target, 10u64, )); - } - CurrentEraInfo::::set(RewardEraInfo { era_index: 25, started_at: 100 }); - assert_ok!(Capacity::change_staking_target( + } + CurrentEraInfo::::set(RewardEraInfo { era_index: 25, started_at: 100 }); + assert_ok!(Capacity::change_staking_target( RuntimeOrigin::signed(staking_account), from_target, to_target, 10u64, )); - }) + }) } #[test] fn change_staking_target_test_parametric_validity() { - new_test_ext().execute_with(|| { - let staked_amount = 10u64; - let from_account = 200u64; - - StakingAccountLedger::::insert( - from_account, - StakingAccountDetails { - active: 20, - total: 20, - unlocking: Default::default(), - last_rewards_claimed_at: None, - stake_change_unlocking: Default::default(), - }, - ); - let from_account_not_staking = 100u64; - let from_target_not_staked: MessageSourceId = 1; - let to_target_not_provider: MessageSourceId = 2; - let from_target: MessageSourceId = 3; - let to_target: MessageSourceId = 4; - setup_provider(&from_account, &from_target_not_staked, &0u64, ProviderBoost); - setup_provider(&from_account, &from_target, &staked_amount, ProviderBoost); - setup_provider(&from_account, &to_target, &staked_amount, ProviderBoost); - - assert_ok!(Capacity::stake( + new_test_ext().execute_with(|| { + let staked_amount = 10u64; + let from_account = 200u64; + + StakingAccountLedger::::insert( + from_account, + StakingAccountDetails { + active: 20, + total: 20, + unlocking: Default::default(), + last_rewards_claimed_at: None, + stake_change_unlocking: Default::default(), + }, + ); + let from_account_not_staking = 100u64; + let from_target_not_staked: MessageSourceId = 1; + let to_target_not_provider: MessageSourceId = 2; + let from_target: MessageSourceId = 3; + let to_target: MessageSourceId = 4; + setup_provider(&from_account, &from_target_not_staked, &0u64, ProviderBoost); + setup_provider(&from_account, &from_target, &staked_amount, ProviderBoost); + setup_provider(&from_account, &to_target, &staked_amount, ProviderBoost); + + assert_ok!(Capacity::stake( RuntimeOrigin::signed(from_account), from_target, staked_amount, ProviderBoost )); - struct TestCase { - from_account: u64, - from_target: MessageSourceId, - to_target: MessageSourceId, - retarget_amount: u64, - expected_err: Error, - } - let test_cases: Vec = vec![ - // from is a provider but account is not staking to it - TestCase { - from_account, - from_target: from_target_not_staked, - to_target, - retarget_amount: staked_amount, - expected_err: Error::::StakerTargetRelationshipNotFound, - }, - // from_account is not staking at all. - TestCase { - from_account: from_account_not_staking, - from_target, - to_target, - retarget_amount: staked_amount, - expected_err: Error::::NotAStakingAccount, - }, - // from and to providers are valid, but zero amount too small - TestCase { - from_account, - from_target, - to_target, - retarget_amount: 0, - expected_err: Error::::StakingAmountBelowMinimum, - }, - // nonzero amount below minimum is still too small - TestCase { - from_account, - from_target, - to_target, - retarget_amount: 9, - expected_err: Error::::StakingAmountBelowMinimum, - }, - // account is staked with from-target, but to-target is not a provider - TestCase { - from_account, - from_target, - to_target: to_target_not_provider, - retarget_amount: staked_amount, - expected_err: Error::::InvalidTarget, - }, - // account doesn't have enough staked to make the transfer - TestCase { - from_account, - from_target, - to_target, - retarget_amount: 999, - expected_err: Error::::InsufficientStakingBalance, - }, - TestCase { - from_account, - from_target, - to_target: from_target, - retarget_amount: 999, - expected_err: Error::::CannotRetargetToSameProvider, - }, - ]; - - for tc in test_cases { - assert_noop!( + struct TestCase { + from_account: u64, + from_target: MessageSourceId, + to_target: MessageSourceId, + retarget_amount: u64, + expected_err: Error, + } + let test_cases: Vec = vec![ + // from is a provider but account is not staking to it + TestCase { + from_account, + from_target: from_target_not_staked, + to_target, + retarget_amount: staked_amount, + expected_err: Error::::StakerTargetRelationshipNotFound, + }, + // from_account is not staking at all. + TestCase { + from_account: from_account_not_staking, + from_target, + to_target, + retarget_amount: staked_amount, + expected_err: Error::::NotAStakingAccount, + }, + // from and to providers are valid, but zero amount too small + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 0, + expected_err: Error::::StakingAmountBelowMinimum, + }, + // nonzero amount below minimum is still too small + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 9, + expected_err: Error::::StakingAmountBelowMinimum, + }, + // account is staked with from-target, but to-target is not a provider + TestCase { + from_account, + from_target, + to_target: to_target_not_provider, + retarget_amount: staked_amount, + expected_err: Error::::InvalidTarget, + }, + // account doesn't have enough staked to make the transfer + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 999, + expected_err: Error::::InsufficientStakingBalance, + }, + TestCase { + from_account, + from_target, + to_target: from_target, + retarget_amount: 999, + expected_err: Error::::CannotRetargetToSameProvider, + }, + ]; + + for tc in test_cases { + assert_noop!( Capacity::change_staking_target( RuntimeOrigin::signed(tc.from_account), tc.from_target, @@ -441,12 +444,12 @@ fn change_staking_target_test_parametric_validity() { ), tc.expected_err ); - } - }); + } + }); } #[test] fn change_staking_target_cannot_switch_staking_type() { - // if you want to switch staking type you must unstake completely and restake regardless of - // whether it is with an existing or new provider. + // if you want to switch staking type you must unstake completely and restake regardless of + // whether it is with an existing or new provider. } diff --git a/pallets/capacity/src/tests/mock.rs b/pallets/capacity/src/tests/mock.rs index 517831743c..ae5a7e5c37 100644 --- a/pallets/capacity/src/tests/mock.rs +++ b/pallets/capacity/src/tests/mock.rs @@ -15,7 +15,7 @@ use sp_runtime::{ traits::{BlakeTwo256, Convert, IdentityLookup}, AccountId32, DispatchError, Perbill, }; -use sp_std::ops::{Mul, Div}; +use sp_std::ops::{Div, Mul}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; diff --git a/pallets/capacity/src/tests/testing_utils.rs b/pallets/capacity/src/tests/testing_utils.rs index b5d288f47a..117c95258c 100644 --- a/pallets/capacity/src/tests/testing_utils.rs +++ b/pallets/capacity/src/tests/testing_utils.rs @@ -1,9 +1,9 @@ use super::mock::*; use frame_support::{assert_ok, traits::Hooks}; +use common_primitives::capacity::StakingType; #[allow(unused)] use sp_runtime::traits::SignedExtension; -use common_primitives::capacity::StakingType; use crate::{BalanceOf, CapacityDetails, Config, Event}; use common_primitives::msa::MessageSourceId; @@ -64,7 +64,12 @@ pub fn create_capacity_account_and_fund( capacity_details } -pub fn setup_provider(staker: &u64, target: &MessageSourceId, amount: &u64, staking_type: StakingType) { +pub fn setup_provider( + staker: &u64, + target: &MessageSourceId, + amount: &u64, + staking_type: StakingType, +) { let provider_name = String::from("Cst-") + target.to_string().as_str(); register_provider(*target, provider_name); if amount.gt(&0u64) { diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index f2323170e5..d5cea4554c 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -360,7 +360,17 @@ pub trait StakingRewardsProvider { /// The information needed to track a Reward Era #[derive( - PartialEq, Eq, Clone, Copy, Default, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, + PartialEq, + Eq, + Clone, + Copy, + Default, + PartialOrd, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, )] pub struct RewardEraInfo where From f070c5a86a9745b9c62e0e700637e2c9a3166e98 Mon Sep 17 00:00:00 2001 From: shannonwells Date: Wed, 11 Oct 2023 15:01:27 -0700 Subject: [PATCH 09/11] fix e2e tests --- integration-tests/capacity/staking.test.ts | 6 +++--- integration-tests/package-lock.json | 2 +- pallets/capacity/src/lib.rs | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/integration-tests/capacity/staking.test.ts b/integration-tests/capacity/staking.test.ts index 5308ebddf9..78eb33f14e 100644 --- a/integration-tests/capacity/staking.test.ts +++ b/integration-tests/capacity/staking.test.ts @@ -214,13 +214,13 @@ describe("Capacity Staking Tests", function () { }); describe("when attempting to stake below the minimum staking requirements", async function () { - it("should fail to stake for InsufficientStakingAmount", async function () { + it("should fail to stake for StakingAmountBelowMinimum", async function () { let stakingKeys = createKeys("stakingKeys"); let providerId = await createMsaAndProvider(stakingKeys, "stakingKeys", 150n * CENTS); let stakeAmount = 1500n; const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, stakeAmount, 'MaximumCapacity'); - await assert.rejects(failStakeObj.fundAndSend(), { name: "InsufficientStakingAmount" }); + await assert.rejects(failStakeObj.fundAndSend(), { name: "StakingAmountBelowMinimum" }); }); }); @@ -230,7 +230,7 @@ describe("Capacity Staking Tests", function () { let providerId = await createMsaAndProvider(stakingKeys, "stakingKeys", ); const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, 0, 'MaximumCapacity'); - await assert.rejects(failStakeObj.fundAndSend(), { name: "ZeroAmountNotAllowed" }); + await assert.rejects(failStakeObj.fundAndSend(), { name: "StakingAmountBelowMinimum" }); }); }); diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json index abe46c2c51..33654111c0 100644 --- a/integration-tests/package-lock.json +++ b/integration-tests/package-lock.json @@ -259,7 +259,7 @@ "node_modules/@frequency-chain/api-augment": { "version": "0.0.0", "resolved": "file:../js/api-augment/dist/frequency-chain-api-augment-0.0.0.tgz", - "integrity": "sha512-LQ+x1euHtTWrM/JmZlmOQwB24vdpkCEJNke32eJCMGvmP8TU9dHUSMOiDr/29EWaniu5dLBWHM+jvFxBpTTJqw==", + "integrity": "sha512-6h47kmfaR+Pnx3bztwgm3QQfma9D7H/06CUWmV08c2Spwxudz7Lw6u+hZbs97x+O3WTssol1GdUrrJjm+LF/2A==", "license": "Apache-2.0", "dependencies": { "@polkadot/api": "^10.9.1", diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index 2d5b093fee..2e27310aff 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -451,10 +451,9 @@ pub mod pallet { requested_amount: BalanceOf, ) -> DispatchResult { let unstaker = ensure_signed(origin)?; - Self::ensure_can_unstake(&unstaker)?; - ensure!(requested_amount > Zero::zero(), Error::::UnstakedAmountIsZero); + Self::ensure_can_unstake(&unstaker)?; let actual_amount = Self::decrease_active_staking_balance(&unstaker, requested_amount)?; let capacity_reduction = Self::reduce_capacity(&unstaker, target, actual_amount)?; From bd3b7b6f374e316a0b0dd2a9f5873264272417dc Mon Sep 17 00:00:00 2001 From: shannonwells Date: Fri, 13 Oct 2023 14:09:51 -0700 Subject: [PATCH 10/11] updates after rebase --- common/primitives/src/capacity.rs | 34 -------- pallets/capacity/src/lib.rs | 18 +---- pallets/capacity/src/tests/eras_tests.rs | 80 +++++++++++++------ .../src/tests/stake_and_deposit_tests.rs | 8 +- pallets/capacity/src/types.rs | 21 +---- runtime/frequency/src/lib.rs | 3 +- 6 files changed, 68 insertions(+), 96 deletions(-) diff --git a/common/primitives/src/capacity.rs b/common/primitives/src/capacity.rs index 66a7dbf931..d361981d7c 100644 --- a/common/primitives/src/capacity.rs +++ b/common/primitives/src/capacity.rs @@ -4,8 +4,6 @@ use frame_support::traits::tokens::Balance; use scale_info::TypeInfo; use sp_api::Decode; use sp_runtime::DispatchError; -use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay}; -use crate::node::{AccountId, Era, Hash}; /// A trait for checking that a target MSA can be staked to. pub trait TargetValidator { @@ -67,35 +65,3 @@ pub enum StakingType { /// and token for the account holder ProviderBoost, } - - -pub trait StakingRewardsProvider { - type Balance: Balance; - /// Return the size of the reward pool for the given era, in token - /// Errors: - /// - EraOutOfRange when `era` is prior to the history retention limit, or greater than the current Era. - fn reward_pool_size(era: Era) -> Self::Balance; - - /// Return the total unclaimed reward in token for `accountId` for `fromEra` --> `toEra`, inclusive - /// Errors: - /// - NotAStakingAccount - /// - EraOutOfRange when fromEra or toEra are prior to the history retention limit, or greater than the current Era. - fn staking_reward_total(accountId: AccountId, fromEra: Era, toEra: Era); - - /// Validate a payout claim for `accountId`, using `proof` and the provided `payload` StakingRewardClaim. - /// Returns whether the claim passes validation. Accounts must first pass `payoutEligible` test. - /// Errors: - /// - NotAStakingAccount - /// - MaxUnlockingChunksExceeded - /// - All other conditions that would prevent a reward from being claimed return 'false' - fn validate_staking_reward_claim(accountId: AccountId, proof: Hash, payload: StakingRewardClaim) -> bool; - - /// Return whether `accountId` can claim a reward. Staking accounts may not claim a reward more than once - /// per Era, may not claim rewards before a complete Era has been staked, and may not claim more rewards past - /// the number of `MaxUnlockingChunks`. - /// Errors: - /// - NotAStakingAccount - /// - MaxUnlockingChunksExceeded - /// - All other conditions that would prevent a reward from being claimed return 'false' - fn payout_eligible(accountId: AccountIdOf) -> bool; -} diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index 2e27310aff..b627679563 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -70,14 +70,12 @@ pub use common_primitives::{ #[cfg(feature = "runtime-benchmarks")] use common_primitives::benchmarks::RegisterProviderBenchmarkHelper; -use common_primitives::{ - capacity::StakingType, - node::{RewardEra}, -}; +use common_primitives::capacity::StakingType; pub use pallet::*; pub use types::*; pub use weights::*; + pub mod types; #[cfg(feature = "runtime-benchmarks")] @@ -85,7 +83,6 @@ mod benchmarking; #[cfg(test)] mod tests; -mod tests; pub mod weights; @@ -96,9 +93,9 @@ const STAKING_ID: LockIdentifier = *b"netstkng"; #[frame_support::pallet] pub mod pallet { use super::*; - use codec::EncodeLike; + use codec::EncodeLike; - use common_primitives::capacity::{StakingType}; + use common_primitives::capacity::StakingType; use frame_support::{pallet_prelude::*, Twox64Concat}; use frame_system::pallet_prelude::*; use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay}; @@ -768,13 +765,6 @@ impl Pallet { } } - /// Returns whether `account_id` may claim and and be paid token rewards. - pub fn payout_eligible(account_id: T::AccountId) -> bool { - let _staking_account = - Self::get_staking_account_for(account_id).ok_or(Error::::StakingAccountNotFound); - false - } - fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight { let current_era_info: RewardEraInfo = Self::get_current_era(); // 1r if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { diff --git a/pallets/capacity/src/tests/eras_tests.rs b/pallets/capacity/src/tests/eras_tests.rs index ad5ffb0501..bc59bdda3a 100644 --- a/pallets/capacity/src/tests/eras_tests.rs +++ b/pallets/capacity/src/tests/eras_tests.rs @@ -1,16 +1,51 @@ -use super::{mock::*, - testing_utils::{run_to_block, system_run_to_block, setup_provider}}; -use crate::{CurrentEraInfo, RewardEraInfo, StakingRewardPool, RewardPoolInfo}; +use super::{ + mock::*, + testing_utils::{run_to_block, system_run_to_block}, +}; +use crate::{Config, CurrentEraInfo, RewardEraInfo, RewardPoolInfo, StakingRewardPool}; +use sp_core::Get; #[test] -fn start_new_era_if_needed_updates_reward_pool_and_era_info() { +fn start_new_era_if_needed_updates_era_info_and_limits_reward_pool_size() { new_test_ext().execute_with(|| { CurrentEraInfo::::set(RewardEraInfo { era_index: 1, started_at: 0 }); - StakingRewardPool::::insert(1, RewardPoolInfo { - total_staked_token: 10_000, - total_reward_pool: 1_000, - unclaimed_balance: 1_000, - }); + StakingRewardPool::::insert( + 1, + RewardPoolInfo { + total_staked_token: 10_000, + total_reward_pool: 1_000, + unclaimed_balance: 1_000, + }, + ); + system_run_to_block(9); + for i in 1..4 { + let block_decade = i * 10; + run_to_block(block_decade); + + let current_era_info = CurrentEraInfo::::get(); + + let expected_era = i + 1; + assert_eq!(current_era_info.era_index, expected_era); + assert_eq!(current_era_info.started_at, block_decade); + let past_eras_max: u32 = ::StakingRewardsPastErasMax::get(); + assert!(StakingRewardPool::::count().le(&past_eras_max)); + system_run_to_block(block_decade + 9); + } + }) +} + +#[test] +fn start_new_era_if_needed_updates_reward_pool() { + new_test_ext().execute_with(|| { + CurrentEraInfo::::set(RewardEraInfo { era_index: 1, started_at: 0 }); + StakingRewardPool::::insert( + 1, + RewardPoolInfo { + total_staked_token: 10_000, + total_reward_pool: 1_000, + unclaimed_balance: 1_000, + }, + ); system_run_to_block(8); // TODO: Provider boost, after staking updates reward pool info @@ -21,22 +56,19 @@ fn start_new_era_if_needed_updates_reward_pool_and_era_info() { system_run_to_block(9); run_to_block(10); - let mut current_era_info = CurrentEraInfo::::get(); - assert_eq!(current_era_info.era_index, 2u32); - assert_eq!(current_era_info.started_at, 10u32); - assert_eq!(StakingRewardPool::::count(), 2); let current_reward_pool_info = StakingRewardPool::::get(2).unwrap(); - assert_eq!(current_reward_pool_info, RewardPoolInfo { - total_staked_token: 10_000, - total_reward_pool: 1_000, - unclaimed_balance: 1_000, - }); + assert_eq!( + current_reward_pool_info, + RewardPoolInfo { + total_staked_token: 10_000, + total_reward_pool: 1_000, + unclaimed_balance: 1_000, + } + ); - system_run_to_block(19); - run_to_block(20); - current_era_info = CurrentEraInfo::::get(); - assert_eq!(current_era_info.era_index, 3u32); - assert_eq!(current_era_info.started_at, 20u32); - }) + // TODO: after staking updates reward pool info + // system_run_to_block(19); + // run_to_block(20); + }); } diff --git a/pallets/capacity/src/tests/stake_and_deposit_tests.rs b/pallets/capacity/src/tests/stake_and_deposit_tests.rs index 40072172f2..23044058d6 100644 --- a/pallets/capacity/src/tests/stake_and_deposit_tests.rs +++ b/pallets/capacity/src/tests/stake_and_deposit_tests.rs @@ -2,7 +2,7 @@ use super::{mock::*, testing_utils::*}; use crate::{BalanceOf, CapacityDetails, Error, Event, StakingAccountDetails}; use common_primitives::{ capacity::{ - Nontransferable, + Nontransferable, StakingType, StakingType::{MaximumCapacity, ProviderBoost}, }, msa::MessageSourceId, @@ -389,7 +389,7 @@ fn increase_stake_and_issue_capacity_errors_with_overflow() { &mut staking_account, &target, &overflow_amount, - &StakingType::ProviderBoost, + &ProviderBoost, ), ArithmeticError::Overflow ); @@ -479,8 +479,8 @@ fn assert_successful_increase_stake_with_type( fn increase_stake_and_issue_capacity_happy_path() { new_test_ext().execute_with(|| { assert_successful_increase_stake_with_type(1, MaximumCapacity, 550, 550, 55); - assert_successful_increase_stake_with_type(2, StakingType::ProviderBoost, 550, 550, 3); - assert_successful_increase_stake_with_type(2, StakingType::ProviderBoost, 6666, 7216, 36); + assert_successful_increase_stake_with_type(2, ProviderBoost, 550, 550, 3); + assert_successful_increase_stake_with_type(2, ProviderBoost, 6666, 7216, 36); }); } diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index d5cea4554c..4324362641 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -1,16 +1,15 @@ //! Types for the Capacity Pallet use super::*; -use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; +use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ log::warn, BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebug, RuntimeDebugNoBound, }; use scale_info::TypeInfo; -use sp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Saturating, Zero}; +use sp_runtime::traits::{CheckedAdd, CheckedSub, Saturating, Zero}; use common_primitives::capacity::StakingType; #[cfg(any(feature = "runtime-benchmarks", test))] use sp_std::vec::Vec; -use common_primitives::node::{AccountId, Hash}; /// The type used for storing information about staking details. #[derive( @@ -372,27 +371,13 @@ pub trait StakingRewardsProvider { TypeInfo, MaxEncodedLen, )] -pub struct RewardEraInfo -where - RewardEra: AtLeast32BitUnsigned + EncodeLike, -{ +pub struct RewardEraInfo { /// the index of this era pub era_index: RewardEra, /// the starting block of this era pub started_at: BlockNumber, } -/// The information needed to track a Reward Era -#[derive( - PartialEq, Eq, Clone, Default, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, -)] -pub struct RewardEraInfo { - /// the index of this era - pub current_era: RewardEra, - /// the starting block of this era - pub era_start: BlockNumber, -} - /// Needed data about a RewardPool for a given RewardEra. /// The total_reward_pool balance for the previous era is set when a new era starts, /// based on total staked token at the end of the previous era, and remains unchanged. diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index 6ba10c4aaf..baee758495 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -78,7 +78,6 @@ pub use common_runtime::{ }; use frame_support::traits::Contains; -use common_primitives::capacity::StakingRewardsProvider; #[cfg(feature = "try-runtime")] use frame_support::traits::TryStateSelect; @@ -740,7 +739,7 @@ use pallet_frequency_tx_payment::Call as FrequencyPaymentCall; use pallet_handles::Call as HandlesCall; use pallet_messages::Call as MessagesCall; use pallet_msa::Call as MsaCall; -use pallet_stateful_storage::{types::ItemAction::Delete, Call as StatefulStorageCall}; +use pallet_stateful_storage::Call as StatefulStorageCall; pub struct CapacityEligibleCalls; impl GetStableWeight for CapacityEligibleCalls { From f4755d57abbfafce106c7cc675663d0745902d94 Mon Sep 17 00:00:00 2001 From: shannonwells Date: Fri, 13 Oct 2023 16:22:22 -0700 Subject: [PATCH 11/11] clean up for PR --- pallets/capacity/src/lib.rs | 25 +++++++++++-------- pallets/capacity/src/tests/eras_tests.rs | 4 +-- pallets/capacity/src/tests/unstaking_tests.rs | 2 +- pallets/capacity/src/types.rs | 11 +++++--- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index b627679563..6bf5c4c797 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -242,8 +242,9 @@ pub mod pallet { pub type EpochLength = StorageValue<_, T::BlockNumber, ValueQuery, EpochLengthDefault>; - /// Information about the current staking reward era. + /// Information about the current staking reward era. Checked every block. #[pallet::storage] + #[pallet::whitelist_storage] #[pallet::getter(fn get_current_era)] pub type CurrentEraInfo = StorageValue<_, RewardEraInfo, ValueQuery>; @@ -767,8 +768,8 @@ impl Pallet { fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight { let current_era_info: RewardEraInfo = Self::get_current_era(); // 1r + if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() { - // 1r let new_era_info = RewardEraInfo { era_index: current_era_info.era_index.saturating_add(One::one()), started_at: current_block, @@ -776,17 +777,17 @@ impl Pallet { let current_reward_pool_info = Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r + let past_eras_max = T::StakingRewardsPastErasMax::get(); - let entries: u32 = StakingRewardPool::::count(); + let entries: u32 = StakingRewardPool::::count(); // 1r + if past_eras_max.eq(&entries.into()) { - // 2r - let current_era = Self::get_current_era().era_index; - let earliest_era = current_era.saturating_sub(past_eras_max).add(One::one()); + let earliest_era = + current_era_info.era_index.saturating_sub(past_eras_max).add(One::one()); StakingRewardPool::::remove(earliest_era); // 1w } CurrentEraInfo::::set(new_era_info); // 1w - // let msa_handle = T::HandleProvider::get_handle_for_msa(msa_id); let total_reward_pool = T::RewardsProvider::reward_pool_size(current_reward_pool_info.total_staked_token); let new_reward_pool = RewardPoolInfo { @@ -797,10 +798,10 @@ impl Pallet { StakingRewardPool::::insert(new_era_info.era_index, new_reward_pool); // 1w T::WeightInfo::on_initialize() - .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) } else { - T::DbWeight::get().reads(2) + T::DbWeight::get().reads(1) } } @@ -821,8 +822,7 @@ impl Pallet { Self::get_staking_account_for(staker).ok_or(Error::::NotAStakingAccount)?; let current_era: T::RewardEra = Self::get_current_era().era_index; - let thaw_eras = T::ChangeStakingTargetThawEras::get(); - let thaw_at = current_era.saturating_add(thaw_eras); + let thaw_at = current_era.saturating_add(T::ChangeStakingTargetThawEras::get()); staking_account_details.update_stake_change_unlocking(amount, &thaw_at, ¤t_era)?; Self::set_staking_account(staker, &staking_account_details); Ok(()) @@ -842,8 +842,11 @@ impl Pallet { let mut to_msa_target = Self::get_target_for(staker, to_msa).unwrap_or_default(); if to_msa_target.amount.is_zero() { + // it's a new StakingTargetDetails record. to_msa_target.staking_type = staking_type.clone(); } else { + // make sure they are not retargeting to a StakingTargetDetails with a different staking + // type, otherwise it could interfere with staking rewards. ensure!( to_msa_target.staking_type.eq(staking_type), Error::::CannotChangeStakingType diff --git a/pallets/capacity/src/tests/eras_tests.rs b/pallets/capacity/src/tests/eras_tests.rs index bc59bdda3a..b6ec94b0e9 100644 --- a/pallets/capacity/src/tests/eras_tests.rs +++ b/pallets/capacity/src/tests/eras_tests.rs @@ -48,7 +48,7 @@ fn start_new_era_if_needed_updates_reward_pool() { ); system_run_to_block(8); - // TODO: Provider boost, after staking updates reward pool info + // TODO: Provider boost, after staking updates reward pool info #1699 // let staker = 10_000; // let provider_msa: MessageSourceId = 1; // let stake_amount = 600u64; @@ -67,7 +67,7 @@ fn start_new_era_if_needed_updates_reward_pool() { } ); - // TODO: after staking updates reward pool info + // TODO: after staking updates reward pool info #1699 // system_run_to_block(19); // run_to_block(20); }); diff --git a/pallets/capacity/src/tests/unstaking_tests.rs b/pallets/capacity/src/tests/unstaking_tests.rs index 035e3f1eee..ee5db7ba80 100644 --- a/pallets/capacity/src/tests/unstaking_tests.rs +++ b/pallets/capacity/src/tests/unstaking_tests.rs @@ -12,7 +12,7 @@ use sp_core::bounded::BoundedVec; #[test] fn unstake_happy_path() { new_test_ext().execute_with(|| { - // TODO: ProviderBoost after unstake affects reward pool info + // TODO: ProviderBoost after unstake affects reward pool info #1699 let token_account = 200; let target: MessageSourceId = 1; let staking_amount = 100; diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index 4324362641..409413593f 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -1,11 +1,11 @@ //! Types for the Capacity Pallet use super::*; -use codec::{Decode, Encode, MaxEncodedLen}; +use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{ log::warn, BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebug, RuntimeDebugNoBound, }; use scale_info::TypeInfo; -use sp_runtime::traits::{CheckedAdd, CheckedSub, Saturating, Zero}; +use sp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Saturating, Zero}; use common_primitives::capacity::StakingType; #[cfg(any(feature = "runtime-benchmarks", test))] @@ -342,7 +342,7 @@ pub trait StakingRewardsProvider { /// Validate a payout claim for `accountId`, using `proof` and the provided `payload` StakingRewardClaim. /// Returns whether the claim passes validation. Accounts must first pass `payoutEligible` test. - /// Errors:: + /// Errors: /// - NotAStakingAccount /// - MaxUnlockingChunksExceeded /// - All other conditions that would prevent a reward from being claimed return 'false' @@ -371,7 +371,10 @@ pub trait StakingRewardsProvider { TypeInfo, MaxEncodedLen, )] -pub struct RewardEraInfo { +pub struct RewardEraInfo +where + RewardEra: AtLeast32BitUnsigned + EncodeLike, +{ /// the index of this era pub era_index: RewardEra, /// the starting block of this era