diff --git a/pallets/capacity/src/benchmarking.rs b/pallets/capacity/src/benchmarking.rs index 5693c2d413..b28cf3c117 100644 --- a/pallets/capacity/src/benchmarking.rs +++ b/pallets/capacity/src/benchmarking.rs @@ -60,10 +60,14 @@ pub fn setup_provider_stake( caller: &T::AccountId, target: &MessageSourceId, staking_amount: BalanceOf, + is_provider_boost: bool, ) { let capacity_amount: BalanceOf = Capacity::::capacity_generated(staking_amount); let mut staking_account = StakingDetails::::default(); + if is_provider_boost { + staking_account.staking_type = ProviderBoost; + } let mut target_details = StakingTargetDetails::>::default(); let mut capacity_details = CapacityDetails::, ::EpochNumber>::default(); @@ -161,8 +165,15 @@ benchmarks! { let target = 1; let block_number = 4u32; + // Adds a boost history entry for this era only so unstake succeeds and there is an update + // to provider boost history. + let mut pbh: ProviderBoostHistory = ProviderBoostHistory::new(); + pbh.add_era_balance(&1u32.into(), &staking_amount); + ProviderBoostHistories::::set(caller.clone(), Some(pbh)); set_era_and_reward_pool_at_block::(1u32.into(), 1u32.into(), 1_000u32.into()); - setup_provider_stake::(&caller, &target, staking_amount); + + setup_provider_stake::(&caller, &target, staking_amount, true); + fill_unlock_chunks::(&caller, T::MaxUnlockingChunks::get() - 1); }: _ (RawOrigin::Signed(caller.clone()), target, unstaking_amount.into()) verify { @@ -193,8 +204,8 @@ benchmarks! { set_era_and_reward_pool_at_block::(1u32.into(), 1u32.into(), 1_000u32.into()); register_provider::(from_msa, "frommsa"); register_provider::(to_msa, "tomsa"); - setup_provider_stake::(&caller, &from_msa, from_msa_amount); - setup_provider_stake::(&caller, &to_msa, to_msa_amount); + setup_provider_stake::(&caller, &from_msa, from_msa_amount, false); + setup_provider_stake::(&caller, &to_msa, to_msa_amount, false); let restake_amount: BalanceOf = from_msa_amount.saturating_sub(10u32.into()); }: _ (RawOrigin::Signed(caller.clone(), ), from_msa, to_msa, restake_amount) diff --git a/pallets/capacity/src/lib.rs b/pallets/capacity/src/lib.rs index a5f419c63b..c415b9f732 100644 --- a/pallets/capacity/src/lib.rs +++ b/pallets/capacity/src/lib.rs @@ -60,7 +60,7 @@ use frame_support::{ use sp_runtime::{ traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero}, - ArithmeticError, DispatchError, Perbill, Permill, + ArithmeticError, BoundedVec, DispatchError, Perbill, Permill, }; pub use common_primitives::{ @@ -187,6 +187,7 @@ pub mod pallet { + MaxEncodedLen + EncodeLike + Into> + + Into> + TypeInfo; /// The number of blocks in a RewardEra @@ -380,6 +381,7 @@ pub mod pallet { /// Staker is attempting to stake an amount below the minimum amount. StakingAmountBelowMinimum, /// Staker is attempting to stake a zero amount. DEPRECATED + /// #[deprecated(since = "1.13.0", note = "Use StakingAmountBelowMinimum instead")] ZeroAmountNotAllowed, /// This AccountId does not have a staking account. NotAStakingAccount, @@ -403,22 +405,23 @@ pub mod pallet { MaxEpochLengthExceeded, /// Staker is attempting to stake an amount that leaves a token balance below the minimum amount. BalanceTooLowtoStake, + /// There are no unstaked token amounts that have passed their thaw period. + NoThawedTokenAvailable, /// Staker tried to change StakingType on an existing account CannotChangeStakingType, /// The Era specified is too far in the past or is in the future (15) 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, + /// There are no rewards eligible to claim. Rewards have expired, have already been + /// claimed, or boosting has never been done before the current era. + NoRewardsEligibleToClaim, /// Caller must wait until the next RewardEra, claim rewards and then can boost again. MustFirstClaimRewards, /// Too many change_staking_target calls made in this RewardEra. (20) MaxRetargetsExceeded, - /// There are no unstaked token amounts that have passed their thaw period. - NoThawedTokenAvailable, + /// Tried to exceed bounds of a some Bounded collection + CollectionBoundExceeded, } #[pallet::hooks] @@ -505,6 +508,8 @@ pub mod pallet { ensure!(requested_amount > Zero::zero(), Error::::UnstakedAmountIsZero); + ensure!(!Self::has_unclaimed_rewards(&unstaker), Error::::MustFirstClaimRewards); + let (actual_amount, staking_type) = Self::decrease_active_staking_balance(&unstaker, requested_amount)?; Self::add_unlock_chunk(&unstaker, actual_amount)?; @@ -798,14 +803,17 @@ impl Pallet { ensure!(amount <= staking_account.active, Error::::InsufficientStakingBalance); let actual_unstaked_amount = staking_account.withdraw(amount)?; - let staking_type = staking_account.staking_type; Self::set_staking_account(unstaker, &staking_account); - let era = Self::get_current_era().era_index; - let mut reward_pool = - Self::get_reward_pool_for_era(era).ok_or(Error::::EraOutOfRange)?; - reward_pool.total_staked_token = reward_pool.total_staked_token.saturating_sub(amount); - Self::set_reward_pool(era, &reward_pool.clone()); + let staking_type = staking_account.staking_type; + if staking_type == ProviderBoost { + let era = Self::get_current_era().era_index; + let mut reward_pool = + Self::get_reward_pool_for_era(era).ok_or(Error::::EraOutOfRange)?; + reward_pool.total_staked_token = reward_pool.total_staked_token.saturating_sub(amount); + Self::set_reward_pool(era, &reward_pool.clone()); + Self::upsert_boost_history(&unstaker, era, actual_unstaked_amount, false)?; + } Ok((actual_unstaked_amount, staking_type)) } @@ -914,14 +922,6 @@ impl Pallet { capacity_details.withdraw(actual_capacity, actual_amount); - if staking_type.eq(&ProviderBoost) { - // update staking history - let era = Self::get_current_era().era_index; - let mut pbh = - Self::get_staking_history_for(unstaker).ok_or(Error::::NotAStakingAccount)?; - pbh.subtract_era_balance(&era, &actual_amount); - ProviderBoostHistories::set(unstaker, Some(pbh)); - } Self::set_capacity_for(target, capacity_details); Self::set_target_details_for(unstaker, target, staking_target_details); @@ -1058,10 +1058,96 @@ impl Pallet { } else { boost_history.subtract_era_balance(¤t_era, &boost_amount) }; - ensure!(upsert_result.is_some(), Error::::EraOutOfRange); - ProviderBoostHistories::::set(account, Some(boost_history)); + match upsert_result { + Some(0usize) => ProviderBoostHistories::::remove(account), + None => return Err(DispatchError::from(Error::::EraOutOfRange)), + _ => ProviderBoostHistories::::set(account, Some(boost_history)), + } Ok(()) } + + pub(crate) fn has_unclaimed_rewards(account: &T::AccountId) -> bool { + let current_era = Self::get_current_era().era_index; + match Self::get_staking_history_for(account) { + Some(provider_boost_history) => { + match provider_boost_history.count() { + 0usize => false, + // they staked before the current era, so they have unclaimed rewards. + 1usize => provider_boost_history.get_entry_for_era(¤t_era).is_none(), + _ => true, + } + }, + None => false, + } // 1r + } + + // this could be up to 35 reads. + #[allow(unused)] + pub(crate) fn list_unclaimed_rewards( + account: &T::AccountId, + ) -> Result, T::StakingRewardsPastErasMax>, DispatchError> { + let mut unclaimed_rewards: BoundedVec< + UnclaimedRewardInfo, + T::StakingRewardsPastErasMax, + > = BoundedVec::new(); + + if !Self::has_unclaimed_rewards(account) { + // 2r + return Ok(unclaimed_rewards); + } + + let staking_history = + Self::get_staking_history_for(account).ok_or(Error::::NotAStakingAccount)?; // cached read from has_unclaimed_rewards + + let era_info = Self::get_current_era(); // cached read, ditto + + let max_history: u32 = T::StakingRewardsPastErasMax::get() - 1; // 1r + let era_length: u32 = T::EraLength::get(); // 1r + let mut reward_era = era_info.era_index.saturating_sub((max_history).into()); + let end_era = era_info.era_index.saturating_sub(One::one()); + // start with how much was staked in the era before the earliest for which there are eligible rewards. + let mut previous_amount: BalanceOf = + staking_history.get_amount_staked_for_era(&(reward_era.saturating_sub(1u32.into()))); + while reward_era.le(&end_era) { + let staked_amount = staking_history.get_amount_staked_for_era(&reward_era); + if !staked_amount.is_zero() { + let expires_at_era = reward_era.saturating_add(max_history.into()); + let reward_pool = + Self::get_reward_pool_for_era(reward_era).ok_or(Error::::EraOutOfRange)?; // 1r + let expires_at_block = if expires_at_era.eq(&era_info.era_index) { + era_info.started_at + era_length.into() // expires at end of this era + } else { + let eras_to_expiration = + expires_at_era.saturating_sub(era_info.era_index).add(1u32.into()); + let blocks_to_expiration = eras_to_expiration * era_length.into(); + let started_at = era_info.started_at; + started_at + blocks_to_expiration.into() + }; + let eligible_amount = if staked_amount.lt(&previous_amount) { + staked_amount + } else { + previous_amount + }; + let earned_amount = ::RewardsProvider::era_staking_reward( + eligible_amount, + reward_pool.total_staked_token, + reward_pool.total_reward_pool, + ); + unclaimed_rewards + .try_push(UnclaimedRewardInfo { + reward_era, + expires_at_block, + eligible_amount, + earned_amount, + }) + .map_err(|_e| Error::::CollectionBoundExceeded)?; + // ^^ there's no good reason for this ever to fail in production but it should be handled. + previous_amount = staked_amount; + } + reward_era = reward_era.saturating_add(One::one()); + } // 1r * up to StakingRewardsPastErasMax-1, if they staked every RewardEra. + Ok(unclaimed_rewards) + } } /// Nontransferable functions are intended for capacity spend and recharge. diff --git a/pallets/capacity/src/tests/eras_tests.rs b/pallets/capacity/src/tests/eras_tests.rs index f9fdcadbcb..14b0862422 100644 --- a/pallets/capacity/src/tests/eras_tests.rs +++ b/pallets/capacity/src/tests/eras_tests.rs @@ -12,8 +12,8 @@ use sp_core::Get; fn start_new_era_if_needed_updates_era_info() { new_test_ext().execute_with(|| { system_run_to_block(9); - for i in 1..4 { - let block_decade = i * 10; + for i in 1..=4 { + let block_decade = (i * 10) + 1; run_to_block(block_decade); let current_era_info = CurrentEraInfo::::get(); @@ -45,7 +45,7 @@ fn start_new_era_if_needed_updates_reward_pool() { for i in 1u32..4 { let era = i + 1; - let final_block = i * 10; + let final_block = (i * 10) + 1; system_run_to_block(final_block - 1); run_to_block(final_block); assert_eq!(StakingRewardPool::::count(), era); diff --git a/pallets/capacity/src/tests/mock.rs b/pallets/capacity/src/tests/mock.rs index 6bcf9384f6..0776fa440c 100644 --- a/pallets/capacity/src/tests/mock.rs +++ b/pallets/capacity/src/tests/mock.rs @@ -1,7 +1,7 @@ use crate as pallet_capacity; use crate::{ - tests::testing_utils::set_era_and_reward_pool_at_block, BalanceOf, StakingRewardClaim, + tests::testing_utils::set_era_and_reward_pool, BalanceOf, StakingRewardClaim, StakingRewardsProvider, }; use common_primitives::{ @@ -208,7 +208,7 @@ impl pallet_capacity::Config for Test { type CapacityPerToken = TestCapacityPerToken; type RewardEra = TestRewardEra; type EraLength = ConstU32<10>; - type StakingRewardsPastErasMax = ConstU32<5>; + type StakingRewardsPastErasMax = ConstU32<6>; // 5 for claiming rewards, 1 for current reward era type RewardsProvider = Capacity; type MaxRetargetsPerRewardEra = ConstU32<5>; type RewardPoolEachEra = ConstU64<10_000>; @@ -235,7 +235,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| { System::set_block_number(1); - set_era_and_reward_pool_at_block(1, 0, 0); + set_era_and_reward_pool(1, 1, 0); }); ext } diff --git a/pallets/capacity/src/tests/provider_boost_history_tests.rs b/pallets/capacity/src/tests/provider_boost_history_tests.rs index f489c56145..bf0c449759 100644 --- a/pallets/capacity/src/tests/provider_boost_history_tests.rs +++ b/pallets/capacity/src/tests/provider_boost_history_tests.rs @@ -33,17 +33,17 @@ fn multiple_provider_boosts_updates_history_correctly() { // should update era 1 history let mut history = Capacity::get_staking_history_for(staker).unwrap(); assert_eq!(history.count(), 1); - assert_eq!(history.get_staking_amount_for_era(&1u32).unwrap(), &700u64); + assert_eq!(history.get_entry_for_era(&1u32).unwrap(), &700u64); - system_run_to_block(9); - run_to_block(10); + system_run_to_block(10); + run_to_block(11); assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(staker), target, 200)); // should add an era 2 history history = Capacity::get_staking_history_for(staker).unwrap(); assert_eq!(history.count(), 2); - assert_eq!(history.get_staking_amount_for_era(&2u32).unwrap(), &900u64); + assert_eq!(history.get_entry_for_era(&2u32).unwrap(), &900u64); }) } @@ -74,10 +74,10 @@ fn provider_boost_history_add_era_balance_adds_entries_and_deletes_old_if_full() let new_era: ::RewardEra = 99; pbh.add_era_balance(&new_era, &1000u64); assert_eq!(pbh.count(), bound as usize); - assert!(pbh.get_staking_amount_for_era(&new_era).is_some()); + assert!(pbh.get_entry_for_era(&new_era).is_some()); let first_era: ::RewardEra = 1; - assert!(pbh.get_staking_amount_for_era(&first_era).is_none()); + assert!(pbh.get_entry_for_era(&first_era).is_none()); } #[test] @@ -87,7 +87,7 @@ fn provider_boost_history_add_era_balance_in_same_era_updates_entry() { let era = 2u32; pbh.add_era_balance(&era, &add_amount); pbh.add_era_balance(&era, &add_amount); - assert_eq!(pbh.get_staking_amount_for_era(&era).unwrap(), &2_000u64) + assert_eq!(pbh.get_entry_for_era(&era).unwrap(), &2_000u64) } #[test] @@ -96,7 +96,7 @@ fn provider_boost_history_subtract_era_balance_in_same_era_updates_entry() { let era = 2u32; pbh.add_era_balance(&era, &1000u64); pbh.subtract_era_balance(&era, &300u64); - assert_eq!(pbh.get_staking_amount_for_era(&(era)).unwrap(), &700u64); + assert_eq!(pbh.get_entry_for_era(&(era)).unwrap(), &700u64); } #[test] @@ -106,7 +106,7 @@ fn provider_boost_history_add_era_balance_in_new_era_correctly_adds_value() { let era = 2u32; pbh.add_era_balance(&era, &add_amount); pbh.add_era_balance(&(era + 1), &add_amount); - assert_eq!(pbh.get_staking_amount_for_era(&(era + 1)).unwrap(), &2_000u64) + assert_eq!(pbh.get_entry_for_era(&(era + 1)).unwrap(), &2_000u64) } #[test] @@ -116,8 +116,17 @@ fn provider_boost_history_subtract_era_balance_in_new_era_correctly_subtracts_va pbh.add_era_balance(&era, &1000u64); pbh.subtract_era_balance(&(era + 1), &400u64); - assert_eq!(pbh.get_staking_amount_for_era(&(era + 1)).unwrap(), &600u64); + assert_eq!(pbh.get_entry_for_era(&(era + 1)).unwrap(), &600u64); pbh.subtract_era_balance(&(era + 2), &600u64); - assert!(pbh.get_staking_amount_for_era(&(era + 2)).unwrap().is_zero()); + assert!(pbh.get_entry_for_era(&(era + 2)).unwrap().is_zero()); +} + +#[test] +fn provider_boost_history_subtract_all_balance_on_only_entry_returns_some_0() { + let mut pbh = ProviderBoostHistory::::new(); + let era = 22u32; + let amount = 1000u64; + pbh.add_era_balance(&era, &amount); + assert_eq!(pbh.subtract_era_balance(&(era), &amount), Some(0usize)); } diff --git a/pallets/capacity/src/tests/rewards_provider_tests.rs b/pallets/capacity/src/tests/rewards_provider_tests.rs index be895a1980..48fce567df 100644 --- a/pallets/capacity/src/tests/rewards_provider_tests.rs +++ b/pallets/capacity/src/tests/rewards_provider_tests.rs @@ -1,10 +1,15 @@ use super::mock::*; use crate::{ - CurrentEraInfo, Error, RewardEraInfo, StakingDetails, StakingRewardClaim, - StakingRewardsProvider, StakingType::*, + CurrentEraInfo, Error, ProviderBoostHistories, ProviderBoostHistory, RewardEraInfo, + StakingDetails, StakingRewardClaim, StakingRewardsProvider, StakingType::*, + UnclaimedRewardInfo, }; -use frame_support::assert_err; +use frame_support::{assert_err, assert_ok, traits::Len}; +use crate::tests::testing_utils::{ + run_to_block, set_era_and_reward_pool, setup_provider, system_run_to_block, +}; +use common_primitives::msa::MessageSourceId; use sp_core::H256; #[test] @@ -97,3 +102,194 @@ fn era_staking_reward_implementation() { ); } } + +#[test] +fn check_for_unclaimed_rewards_returns_empty_set_when_no_staking() { + new_test_ext().execute_with(|| { + let account = 500u64; + let history: ProviderBoostHistory = ProviderBoostHistory::new(); + ProviderBoostHistories::::set(account, Some(history)); + let rewards = Capacity::list_unclaimed_rewards(&account).unwrap(); + assert!(rewards.is_empty()) + }) +} +#[test] +fn check_for_unclaimed_rewards_returns_empty_set_when_only_staked_this_era() { + new_test_ext().execute_with(|| { + system_run_to_block(5); + set_era_and_reward_pool(5u32, 1u32, 1000u64); + let account = 500u64; + let mut history: ProviderBoostHistory = ProviderBoostHistory::new(); + history.add_era_balance(&5u32, &100u64); + ProviderBoostHistories::::set(account, Some(history)); + let rewards = Capacity::list_unclaimed_rewards(&account).unwrap(); + assert!(rewards.is_empty()) + }) +} + +// Check that eligible amounts are only for what's staked an entire era. +#[test] +fn check_for_unclaimed_rewards_has_eligible_rewards() { + new_test_ext().execute_with(|| { + let account = 10_000u64; + let target: MessageSourceId = 10; + let amount = 1_000u64; + + // staking 1k as of block 1, era 1 (1-10) + setup_provider(&account, &target, &amount, ProviderBoost); + + // staking 2k as of block 11, era 2 (11-20) + run_to_block(11); + assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(account), target, amount)); + + // staking 3k as of era 4, block 31, first block of era (31-40) + run_to_block(31); + assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(account), target, amount)); + + run_to_block(51); + assert_eq!(Capacity::get_current_era().era_index, 6u32); + assert_eq!(Capacity::get_current_era().started_at, 51u32); + + assert!(Capacity::get_reward_pool_for_era(0u32).is_none()); + assert!(Capacity::get_reward_pool_for_era(1u32).is_some()); + assert!(Capacity::get_reward_pool_for_era(6u32).is_some()); + + // rewards for era 6 should not be returned; era 6 is current era and therefore ineligible. + // eligible amounts for rewards for eras should be: 1=0, 2=1k, 3=2k, 4=2k, 5=3k + let rewards = Capacity::list_unclaimed_rewards(&account).unwrap(); + assert_eq!(rewards.len(), 5usize); + let expected_info: [UnclaimedRewardInfo; 5] = [ + UnclaimedRewardInfo { + reward_era: 1u32, + expires_at_block: 61, + eligible_amount: 0, + earned_amount: 0, + }, + UnclaimedRewardInfo { + reward_era: 2u32, + expires_at_block: 71, + eligible_amount: 1000, + earned_amount: 4, + }, + UnclaimedRewardInfo { + reward_era: 3u32, + expires_at_block: 81, + eligible_amount: 2_000, + earned_amount: 8, + }, + UnclaimedRewardInfo { + reward_era: 4u32, + expires_at_block: 91, + eligible_amount: 2000, + earned_amount: 8, + }, + UnclaimedRewardInfo { + reward_era: 5u32, + expires_at_block: 101, + eligible_amount: 3_000, + earned_amount: 11, + }, + ]; + for i in 0..=4 { + assert_eq!(rewards.get(i).unwrap(), &expected_info[i]); + } + + run_to_block(61); + let rewards = Capacity::list_unclaimed_rewards(&account).unwrap(); + // the earliest era should no longer be stored. + assert_eq!(rewards.len(), 5usize); + assert_eq!(rewards.get(0).unwrap().reward_era, 2u32); + + // there was no change in stake, so the eligible and earned amounts should be the same as in + // reward era 5. + assert_eq!( + rewards.get(4).unwrap(), + &UnclaimedRewardInfo { + reward_era: 6u32, + expires_at_block: 111, + eligible_amount: 3_000, + earned_amount: 11, + } + ) + }) +} + +// check that if an account boosted and then let it run for more than the number +// of history retention eras, eligible rewards are correct. +#[test] +fn check_for_unclaimed_rewards_returns_correctly_for_old_single_boost() { + new_test_ext().execute_with(|| { + let account = 10_000u64; + let target: MessageSourceId = 10; + let amount = 1_000u64; + + assert!(!Capacity::has_unclaimed_rewards(&account)); + + // boost 1k as of block 1, era 1 + setup_provider(&account, &target, &amount, ProviderBoost); + assert!(!Capacity::has_unclaimed_rewards(&account)); + + run_to_block(71); + assert_eq!(Capacity::get_current_era().era_index, 8u32); + assert_eq!(Capacity::get_current_era().started_at, 71u32); + + let rewards = Capacity::list_unclaimed_rewards(&account).unwrap(); + // the earliest era should no longer be stored. + assert_eq!(rewards.len(), 5usize); + for i in 0..=4 { + let expected_era: u32 = i + 3; + let expected_info: UnclaimedRewardInfo = UnclaimedRewardInfo { + reward_era: expected_era.into(), + expires_at_block: (expected_era * 10u32 + 51u32).into(), + eligible_amount: 1000, + earned_amount: 4, + }; + assert_eq!(rewards.get(i as usize).unwrap(), &expected_info); + } + }) +} + +#[test] +fn has_unclaimed_rewards_works() { + new_test_ext().execute_with(|| { + let account = 10_000u64; + let target: MessageSourceId = 10; + let amount = 1_000u64; + + assert!(!Capacity::has_unclaimed_rewards(&account)); + + // staking 1k as of block 1, era 1 + setup_provider(&account, &target, &amount, ProviderBoost); + assert!(!Capacity::has_unclaimed_rewards(&account)); + + // staking 2k as of block 11, era 2 + run_to_block(11); + assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(account), target, amount)); + assert!(Capacity::has_unclaimed_rewards(&account)); + + // staking 3k as of era 4, block 31 + run_to_block(31); + assert!(Capacity::has_unclaimed_rewards(&account)); + + run_to_block(61); + assert!(Capacity::has_unclaimed_rewards(&account)); + }) +} + +#[test] +fn has_unclaimed_rewards_returns_true_with_old_single_boost() { + new_test_ext().execute_with(|| { + let account = 10_000u64; + let target: MessageSourceId = 10; + let amount = 1_000u64; + + assert!(!Capacity::has_unclaimed_rewards(&account)); + + // boost 1k as of block 1, era 1 + setup_provider(&account, &target, &amount, ProviderBoost); + assert!(!Capacity::has_unclaimed_rewards(&account)); + + run_to_block(71); + assert!(Capacity::has_unclaimed_rewards(&account)); + }); +} diff --git a/pallets/capacity/src/tests/testing_utils.rs b/pallets/capacity/src/tests/testing_utils.rs index 81b3a9a6b1..863906427d 100644 --- a/pallets/capacity/src/tests/testing_utils.rs +++ b/pallets/capacity/src/tests/testing_utils.rs @@ -94,7 +94,7 @@ pub fn setup_provider( } // Currently the reward pool is a constant, however it could change in the future. -pub fn set_era_and_reward_pool_at_block(era_index: u32, started_at: u32, total_staked_token: u64) { +pub fn set_era_and_reward_pool(era_index: u32, started_at: u32, total_staked_token: u64) { let era_info = RewardEraInfo { era_index, started_at }; let total_reward_pool = 10_000u64; CurrentEraInfo::::set(era_info); diff --git a/pallets/capacity/src/tests/unstaking_tests.rs b/pallets/capacity/src/tests/unstaking_tests.rs index ecbdc73dd4..c3eee42209 100644 --- a/pallets/capacity/src/tests/unstaking_tests.rs +++ b/pallets/capacity/src/tests/unstaking_tests.rs @@ -1,8 +1,8 @@ use super::{mock::*, testing_utils::*}; use crate as pallet_capacity; use crate::{ - CapacityDetails, FreezeReason, RewardPoolInfo, StakingDetails, StakingTargetDetails, - StakingType, UnlockChunk, + CapacityDetails, FreezeReason, ProviderBoostHistory, RewardPoolInfo, StakingDetails, + StakingTargetDetails, StakingType, StakingType::ProviderBoost, UnlockChunk, }; use common_primitives::msa::MessageSourceId; use frame_support::{ @@ -158,8 +158,17 @@ fn unstake_errors_unstaking_amount_is_zero() { }); } +fn fill_unstake_unlock_chunks(token_account: u64, target: MessageSourceId, unstaking_amount: u64) { + for _n in 0..::MaxUnlockingChunks::get() { + assert_ok!(Capacity::unstake( + RuntimeOrigin::signed(token_account), + target, + unstaking_amount + )); + } +} #[test] -fn unstake_errors_max_unlocking_chunks_exceeded() { +fn unstake_errors_max_unlocking_chunks_exceeded_stake() { new_test_ext().execute_with(|| { let token_account = 200; let target: MessageSourceId = 1; @@ -170,13 +179,31 @@ fn unstake_errors_max_unlocking_chunks_exceeded() { assert_ok!(Capacity::stake(RuntimeOrigin::signed(token_account), target, staking_amount)); - for _n in 0..::MaxUnlockingChunks::get() { - assert_ok!(Capacity::unstake( - RuntimeOrigin::signed(token_account), - target, - unstaking_amount - )); - } + fill_unstake_unlock_chunks(token_account, target, unstaking_amount); + + assert_noop!( + Capacity::unstake(RuntimeOrigin::signed(token_account), target, unstaking_amount), + Error::::MaxUnlockingChunksExceeded + ); + }); +} +#[test] +fn unstake_errors_max_unlocking_chunks_exceeded_provider_boost() { + new_test_ext().execute_with(|| { + let token_account = 200; + let target: MessageSourceId = 1; + let staking_amount = 60; + let unstaking_amount = 10; + + register_provider(target, String::from("Test Target")); + + assert_ok!(Capacity::provider_boost( + RuntimeOrigin::signed(token_account), + target, + staking_amount + )); + + fill_unstake_unlock_chunks(token_account, target, unstaking_amount); assert_noop!( Capacity::unstake(RuntimeOrigin::signed(token_account), target, unstaking_amount), @@ -283,6 +310,38 @@ fn unstake_provider_boosted_target_adjusts_reward_pool_total() { }); } +#[test] +fn unstake_maximum_does_not_change_reward_pool() { + new_test_ext().execute_with(|| { + // two accounts staking to the same target + let account1 = 600; + let a_booster = 500; + let target: MessageSourceId = 1; + let amount1 = 500; + let unstake_amount = 200; + + let expected_reward_pool: RewardPoolInfo> = RewardPoolInfo { + total_staked_token: 490, // ??? + total_reward_pool: 10_000, + unclaimed_balance: 10_000, + }; + + register_provider(target, String::from("Foo")); + run_to_block(5); // ensures Capacity::on_initialize is run + + assert_ok!(Capacity::stake(RuntimeOrigin::signed(account1), target, amount1)); + assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(a_booster), target, amount1)); + + // there should be only the one contribution + let mut reward_pool = Capacity::get_reward_pool_for_era(1).unwrap(); + assert_eq!(reward_pool, expected_reward_pool); + + assert_ok!(Capacity::unstake(RuntimeOrigin::signed(account1), target, unstake_amount)); + reward_pool = Capacity::get_reward_pool_for_era(1).unwrap(); + assert_eq!(reward_pool, expected_reward_pool); + }); +} + #[test] fn unstake_fills_up_common_unlock_for_any_target() { new_test_ext().execute_with(|| { @@ -308,7 +367,10 @@ fn unstake_fills_up_common_unlock_for_any_target() { }) } +// This fails now because unstaking is disallowed before claiming unclaimed rewards. +// TODO: add claim_rewards call after it's implemented and un-ignore. #[test] +#[ignore] fn unstake_by_a_booster_updates_provider_boost_history_with_correct_amount() { new_test_ext().execute_with(|| { let staker = 10_000; @@ -325,7 +387,7 @@ fn unstake_by_a_booster_updates_provider_boost_history_with_correct_amount() { assert_ok!(Capacity::unstake(RuntimeOrigin::signed(staker), target1, 50)); pbh = Capacity::get_staking_history_for(staker).unwrap(); assert_eq!(pbh.count(), 2); - assert_eq!(pbh.get_staking_amount_for_era(&2u32).unwrap(), &950u64); + assert_eq!(pbh.get_entry_for_era(&2u32).unwrap(), &950u64); }) } @@ -337,6 +399,88 @@ fn unstake_maximum_does_not_change_provider_boost_history() { register_provider(target1, String::from("Test Target")); assert_ok!(Capacity::stake(RuntimeOrigin::signed(staker), target1, 1_000)); + assert_ok!(Capacity::unstake(RuntimeOrigin::signed(staker), target1, 500)); assert!(Capacity::get_staking_history_for(staker).is_none()); }) } + +// Simulate a series of stake/unstake events over 10 eras then check for +// correct staking values, including for eras that do not have an explicit entry. +#[test] +fn get_amount_staked_for_era_works() { + let mut staking_history: ProviderBoostHistory = ProviderBoostHistory::new(); + + for i in 10u32..=13u32 { + staking_history.add_era_balance(&i.into(), &5u64); + } + assert_eq!(staking_history.get_amount_staked_for_era(&10u32), 5u64); + assert_eq!(staking_history.get_amount_staked_for_era(&13u32), 20u64); + + staking_history.subtract_era_balance(&14u32, &7u64); + assert_eq!(staking_history.get_amount_staked_for_era(&14u32), 13u64); + assert_eq!(staking_history.get_amount_staked_for_era(&15u32), 13u64); + + staking_history.add_era_balance(&15u32, &10u64); + + let expected_balance = 23u64; + assert_eq!(staking_history.get_amount_staked_for_era(&15u32), expected_balance); + + // unstake everything + staking_history.subtract_era_balance(&20u32, &expected_balance); + + assert_eq!(staking_history.get_amount_staked_for_era(&16u32), expected_balance); + assert_eq!(staking_history.get_amount_staked_for_era(&17u32), expected_balance); + assert_eq!(staking_history.get_amount_staked_for_era(&18u32), expected_balance); + assert_eq!(staking_history.get_amount_staked_for_era(&19u32), expected_balance); + + // from 20 onward, should return 0. + assert_eq!(staking_history.get_amount_staked_for_era(&20u32), 0u64); + assert_eq!(staking_history.get_amount_staked_for_era(&31u32), 0u64); + + // ensure reporting from earlier is still correct. + assert_eq!(staking_history.get_amount_staked_for_era(&14u32), 13u64); + + // querying for an era that has been cleared due to the hitting the bound + // (StakingRewardsPastErasMax = 5 in mock) returns zero. + assert_eq!(staking_history.get_amount_staked_for_era(&9u32), 0u64); +} + +#[test] +fn unstake_fails_if_provider_boosted_and_have_unclaimed_rewards() { + new_test_ext().execute_with(|| { + let account = 10_000u64; + let target: MessageSourceId = 10; + let amount = 1_000u64; + + // staking 1k as of block 1, era 1 + setup_provider(&account, &target, &amount, ProviderBoost); + + // staking 2k as of block 11, era 2 + run_to_block(11); + assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(account), target, amount)); + + // staking 3k as of era 4, block 31 + run_to_block(31); + + assert_noop!( + Capacity::unstake(RuntimeOrigin::signed(account), target, amount), + Error::::MustFirstClaimRewards + ); + }) +} + +#[test] +fn unstake_all_if_no_unclaimed_rewards_removes_provider_boost_history() { + new_test_ext().execute_with(|| { + let account = 10_000u64; + let target: MessageSourceId = 10; + let amount = 1_000u64; + + // staking 1k as of block 1, era 1 + setup_provider(&account, &target, &amount, ProviderBoost); + assert!(Capacity::get_staking_history_for(account).is_some()); + run_to_block(10); + assert_ok!(Capacity::unstake(RuntimeOrigin::signed(account), target, amount)); + assert!(Capacity::get_staking_history_for(account).is_none()); + }); +} diff --git a/pallets/capacity/src/types.rs b/pallets/capacity/src/types.rs index 0237ad0073..f46ff29b60 100644 --- a/pallets/capacity/src/types.rs +++ b/pallets/capacity/src/types.rs @@ -4,7 +4,7 @@ use frame_support::{BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound use parity_scale_codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Get, Saturating}, + traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Get, Saturating, Zero}, BoundedBTreeMap, RuntimeDebug, }; @@ -261,6 +261,7 @@ pub fn unlock_chunks_from_vec(chunks: &Vec<(u32, u32)>) -> UnlockChun pub struct RewardEraInfo where RewardEra: AtLeast32BitUnsigned + EncodeLike, + BlockNumber: AtLeast32BitUnsigned + EncodeLike, { /// the index of this era pub era_index: RewardEra, @@ -285,9 +286,6 @@ pub struct RewardPoolInfo { pub unclaimed_balance: Balance, } -// TODO: Store retarget unlock chunks in their own storage but can be for any stake type -// TODO: Store unstake unlock chunks in their own storage but can be for any stake type (at first do only boosted unstake) - /// A record of staked amounts for a complete RewardEra #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] @@ -336,6 +334,7 @@ impl ProviderBoostHistory { /// Subtracts `subtract_amount` from the entry for `reward_era`. Zero values are still retained. /// Returns None if there is no entry for the reward era. + /// Returns Some(0) if they unstaked everything and this is the only entry /// Otherwise returns Some(history_count) pub fn subtract_era_balance( &mut self, @@ -345,8 +344,16 @@ impl ProviderBoostHistory { if self.count().is_zero() { return None; }; + // have to save this because calling self.count() after self.0.get_mut produces compiler error + let current_count = self.count(); + if let Some(entry) = self.0.get_mut(reward_era) { *entry = entry.saturating_sub(*subtract_amount); + // if they unstaked everything and there are no other entries, return 0 count (a lie) + // so that the storage can be reaped. + if current_count.eq(&1usize) && entry.is_zero() { + return Some(0usize); + } } else { self.remove_oldest_entry_if_full(); let current_staking_amount = self.get_last_staking_amount(); @@ -361,11 +368,33 @@ impl ProviderBoostHistory { Some(self.count()) } - /// Return how much is staked for the given era. If there is no entry for that era, return None. - pub fn get_staking_amount_for_era(&self, reward_era: &T::RewardEra) -> Option<&BalanceOf> { + /// A wrapper for the key/value retrieval of the BoundedBTreeMap. + pub(crate) fn get_entry_for_era(&self, reward_era: &T::RewardEra) -> Option<&BalanceOf> { self.0.get(reward_era) } + /// Returns how much was staked during the given era, even if there is no explicit entry for that era. + /// If there is no history entry for `reward_era`, returns the next earliest entry's staking balance. + /// + /// Note there is no sense of what the current era is; subsequent calls could return a different result + /// if 'reward_era' is the current era and there has been a boost or unstake. + pub(crate) fn get_amount_staked_for_era(&self, reward_era: &T::RewardEra) -> BalanceOf { + // this gives an ordered-by-key Iterator + let mut bmap_iter = self.0.iter(); + let mut eligible_amount: BalanceOf = Zero::zero(); + while let Some((era, balance)) = bmap_iter.next() { + if era.eq(reward_era) { + return *balance + } + // there was a boost or unstake in this era. + else if era.gt(reward_era) { + return eligible_amount; + } // eligible_amount has been staked through reward_era + eligible_amount = *balance; + } + eligible_amount + } + /// Returns the number of history items pub fn count(&self) -> usize { self.0.len() @@ -489,3 +518,20 @@ pub trait StakingRewardsProvider { /// The amount is multiplied by a factor > 0 and < 1. fn capacity_boost(amount: BalanceOf) -> BalanceOf; } + +/// Result of checking a Boost History item to see if it's eligible for a reward. +#[derive( + Copy, Clone, Encode, Eq, Decode, Default, RuntimeDebug, MaxEncodedLen, PartialEq, TypeInfo, +)] +#[scale_info(skip_type_params(T))] +pub struct UnclaimedRewardInfo { + /// The Reward Era for which this reward was earned + pub reward_era: T::RewardEra, + /// When this reward expires, i.e. can no longer be claimed + pub expires_at_block: BlockNumberFor, + /// The amount staked in this era that is eligible for rewards. Does not count additional amounts + /// staked in this era. + pub eligible_amount: BalanceOf, + /// The amount in token of the reward (only if it can be calculated using only on chain data) + pub earned_amount: BalanceOf, +} diff --git a/pallets/capacity/src/weights.rs b/pallets/capacity/src/weights.rs index 88d8039664..9aebf50495 100644 --- a/pallets/capacity/src/weights.rs +++ b/pallets/capacity/src/weights.rs @@ -18,9 +18,9 @@ //! Autogenerated weights for pallet_capacity //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2024-05-10, STEPS: `20`, REPEAT: `10`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2024-05-21, STEPS: `20`, REPEAT: `10`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `UL-Mac.local`, CPU: `` +//! HOSTNAME: `UL-Mac`, CPU: `` //! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("frequency-bench"), DB CACHE: 1024 // Executed Command: @@ -80,8 +80,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `174` // Estimated: `6249` - // Minimum execution time: 36_000_000 picoseconds. - Weight::from_parts(37_000_000, 6249) + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(38_000_000, 6249) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -121,13 +121,15 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Capacity::CounterForStakingRewardPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) fn start_new_reward_era_if_needed() -> Weight { // Proof Size summary in bytes: - // Measured: `638` + // Measured: `613` // Estimated: `10080` // Minimum execution time: 14_000_000 picoseconds. Weight::from_parts(15_000_000, 10080) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// Storage: `Capacity::ProviderBoostHistories` (r:1 w:1) + /// Proof: `Capacity::ProviderBoostHistories` (`max_values`: None, `max_size`: Some(661), added: 3136, mode: `MaxEncodedLen`) /// Storage: `Capacity::StakingAccountLedger` (r:1 w:1) /// Proof: `Capacity::StakingAccountLedger` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) /// Storage: `Capacity::StakingRewardPool` (r:1 w:1) @@ -140,12 +142,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Capacity::CapacityLedger` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) fn unstake() -> Weight { // Proof Size summary in bytes: - // Measured: `343` - // Estimated: `5071` - // Minimum execution time: 29_000_000 picoseconds. - Weight::from_parts(30_000_000, 5071) - .saturating_add(T::DbWeight::get().reads(5_u64)) - .saturating_add(T::DbWeight::get().writes(5_u64)) + // Measured: `393` + // Estimated: `5611` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(38_000_000, 5611) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) } /// Storage: `Capacity::EpochLength` (r:0 w:1) /// Proof: `Capacity::EpochLength` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -171,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `315` // Estimated: `7601` - // Minimum execution time: 30_000_000 picoseconds. - Weight::from_parts(31_000_000, 7601) + // Minimum execution time: 31_000_000 picoseconds. + Weight::from_parts(32_000_000, 7601) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -193,13 +195,13 @@ impl WeightInfo for SubstrateWeight { /// Storage: `Balances::Locks` (r:1 w:0) /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) /// Storage: `Capacity::ProviderBoostHistories` (r:1 w:1) - /// Proof: `Capacity::ProviderBoostHistories` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Proof: `Capacity::ProviderBoostHistories` (`max_values`: None, `max_size`: Some(661), added: 3136, mode: `MaxEncodedLen`) fn provider_boost() -> Weight { // Proof Size summary in bytes: // Measured: `247` // Estimated: `6249` - // Minimum execution time: 45_000_000 picoseconds. - Weight::from_parts(46_000_000, 6249) + // Minimum execution time: 46_000_000 picoseconds. + Weight::from_parts(47_000_000, 6249) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -225,8 +227,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `174` // Estimated: `6249` - // Minimum execution time: 36_000_000 picoseconds. - Weight::from_parts(37_000_000, 6249) + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(38_000_000, 6249) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -266,13 +268,15 @@ impl WeightInfo for () { /// Proof: `Capacity::CounterForStakingRewardPool` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) fn start_new_reward_era_if_needed() -> Weight { // Proof Size summary in bytes: - // Measured: `638` + // Measured: `613` // Estimated: `10080` // Minimum execution time: 14_000_000 picoseconds. Weight::from_parts(15_000_000, 10080) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + /// Storage: `Capacity::ProviderBoostHistories` (r:1 w:1) + /// Proof: `Capacity::ProviderBoostHistories` (`max_values`: None, `max_size`: Some(661), added: 3136, mode: `MaxEncodedLen`) /// Storage: `Capacity::StakingAccountLedger` (r:1 w:1) /// Proof: `Capacity::StakingAccountLedger` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) /// Storage: `Capacity::StakingRewardPool` (r:1 w:1) @@ -285,12 +289,12 @@ impl WeightInfo for () { /// Proof: `Capacity::CapacityLedger` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) fn unstake() -> Weight { // Proof Size summary in bytes: - // Measured: `343` - // Estimated: `5071` - // Minimum execution time: 29_000_000 picoseconds. - Weight::from_parts(30_000_000, 5071) - .saturating_add(RocksDbWeight::get().reads(5_u64)) - .saturating_add(RocksDbWeight::get().writes(5_u64)) + // Measured: `393` + // Estimated: `5611` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(38_000_000, 5611) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) } /// Storage: `Capacity::EpochLength` (r:0 w:1) /// Proof: `Capacity::EpochLength` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -316,8 +320,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `315` // Estimated: `7601` - // Minimum execution time: 30_000_000 picoseconds. - Weight::from_parts(31_000_000, 7601) + // Minimum execution time: 31_000_000 picoseconds. + Weight::from_parts(32_000_000, 7601) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } @@ -338,13 +342,13 @@ impl WeightInfo for () { /// Storage: `Balances::Locks` (r:1 w:0) /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) /// Storage: `Capacity::ProviderBoostHistories` (r:1 w:1) - /// Proof: `Capacity::ProviderBoostHistories` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Proof: `Capacity::ProviderBoostHistories` (`max_values`: None, `max_size`: Some(661), added: 3136, mode: `MaxEncodedLen`) fn provider_boost() -> Weight { // Proof Size summary in bytes: // Measured: `247` // Estimated: `6249` - // Minimum execution time: 45_000_000 picoseconds. - Weight::from_parts(46_000_000, 6249) + // Minimum execution time: 46_000_000 picoseconds. + Weight::from_parts(47_000_000, 6249) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index 6b19f9db80..260db9b18b 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -561,7 +561,7 @@ impl pallet_capacity::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type RewardEra = u32; type EraLength = ConstU32<{ 14 * DAYS }>; - type StakingRewardsPastErasMax = ConstU32<30u32>; + type StakingRewardsPastErasMax = ConstU32<31u32>; // 30 for claiming rewards, 1 for current era type RewardsProvider = Capacity; type MaxRetargetsPerRewardEra = ConstU32<16>; // Value determined by desired inflation rate limits for chosen economic model