From 6b312335a846c32326508f1d2f2419aa51ca26ec Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:37:13 +0100 Subject: [PATCH] feat: BonusStatus implementation --- pallets/dapp-staking/README.md | 36 +- pallets/dapp-staking/src/lib.rs | 22 +- pallets/dapp-staking/src/test/mock.rs | 5 +- .../dapp-staking/src/test/testing_utils.rs | 81 +++-- pallets/dapp-staking/src/test/tests.rs | 63 +++- pallets/dapp-staking/src/test/tests_types.rs | 308 ++++++++++++++++-- pallets/dapp-staking/src/types.rs | 160 ++++++++- pallets/inflation/src/lib.rs | 2 +- precompiles/dapp-staking/src/lib.rs | 8 +- precompiles/dapp-staking/src/test/mock.rs | 3 +- runtime/astar/src/lib.rs | 3 +- runtime/local/src/lib.rs | 7 +- runtime/shibuya/src/lib.rs | 3 +- runtime/shiden/src/lib.rs | 5 +- tests/xcm-simulator/src/mocks/parachain.rs | 5 +- 15 files changed, 604 insertions(+), 107 deletions(-) diff --git a/pallets/dapp-staking/README.md b/pallets/dapp-staking/README.md index b78827acde..2857ff22e8 100644 --- a/pallets/dapp-staking/README.md +++ b/pallets/dapp-staking/README.md @@ -21,8 +21,9 @@ After an era ends, it's usually possible to claim rewards for it, if user or dAp Periods are another _time unit_ in dApp staking. They are expected to be more lengthy than eras. Each period consists of two subperiods: -* `Voting` -* `Build&Earn` + +- `Voting` +- `Build&Earn` Each period is denoted by a number, which increments each time a new period begins. Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. @@ -41,8 +42,9 @@ Casting a vote, or staking, during the `Voting` subperiod makes the staker eligi `Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting subperiod is treated as a single _voting era_. E.g. if `voting` subperiod lasts for **5 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **500** blocks. -* Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts -* Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts + +- Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts +- Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts Neither stakers nor dApps earn rewards during this subperiod - no new rewards are generated after `voting` subperiod ends. @@ -56,14 +58,15 @@ It is still possible to _stake_ during this period, and stakers are encouraged t The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era which would be in the next period). To continue the previous example where era length is **100** blocks, let's assume that `Build&Earn` subperiod lasts for 10 eras: -* Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts -* Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts -* Block 601, Era 3 starts, Period 1 continues, `Build&Earn` subperiod continues -* Block 701, Era 4 starts, Period 1 continues, `Build&Earn` subperiod continues -* ... -* Block 1401, Era 11 starts, Period 1 continues, `Build&Earn` subperiod enters the final era -* Block 1501, Era 12 starts, Period 2 starts, `Voting` subperiod starts -* Block 2001, Era 13 starts, Period 2 continues, `Build&Earn` subperiod starts + +- Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts +- Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts +- Block 601, Era 3 starts, Period 1 continues, `Build&Earn` subperiod continues +- Block 701, Era 4 starts, Period 1 continues, `Build&Earn` subperiod continues +- ... +- Block 1401, Era 11 starts, Period 1 continues, `Build&Earn` subperiod enters the final era +- Block 1501, Era 12 starts, Period 2 starts, `Voting` subperiod starts +- Block 2001, Era 13 starts, Period 2 continues, `Build&Earn` subperiod starts ### dApps & Smart Contracts @@ -137,11 +140,14 @@ User's stake on a contract must be equal or greater than the `MinimumStakeAmount Although user can stake on multiple smart contracts, the amount is limited. To be more precise, amount of database entries that can exist per user is limited. -The protocol keeps track of how much was staked by the user in `voting` and `build&earn` subperiod. This is important for the bonus reward calculation. +The protocol keeps track of how much was staked by the user in `voting` and `build&earn` subperiod. This is important for the bonus reward calculation. Only a limited number of _move actions_ are allowed during the `build&earn` subperiod to preserve bonus reward elegibility. _Move actions_ refer either to: + +- a 'partial unstake with voting stake decrease', +- a 'stake transfer between two contracts'. It is not possible to stake on a dApp that has been unregistered. However, if dApp is unregistered after user has staked on it, user will keep earning -rewards for the staked amount. +rewards for the staked amount, or can 'move' his stake without impacting his number of allowed 'move actions' for the ongoing period. #### Unstaking Tokens @@ -240,4 +246,4 @@ In case they don't, they will simply miss on the earnings. However, this should not be a problem given how the system is designed. There is no longer _stake&forger_ - users are expected to revisit dApp staking at least at the beginning of each new period to pick out old or new dApps on which to stake on. -If they don't do that, they miss out on the bonus reward & won't earn staker rewards. \ No newline at end of file +If they don't do that, they miss out on the bonus reward & won't earn staker rewards. diff --git a/pallets/dapp-staking/src/lib.rs b/pallets/dapp-staking/src/lib.rs index d96ff175bc..932f79291c 100644 --- a/pallets/dapp-staking/src/lib.rs +++ b/pallets/dapp-staking/src/lib.rs @@ -211,6 +211,12 @@ pub mod pallet { #[pallet::constant] type RankingEnabled: Get; + /// The maximum number of 'move actions' allowed within a single period while + /// retaining eligibility for bonus rewards. Exceeding this limit will result in the + /// forfeiture of the bonus rewards for the affected stake. + #[pallet::constant] + type MaxBonusMovesPerPeriod: Get + Default + Debug; + /// Weight info for various calls & operations in the pallet. type WeightInfo: WeightInfo; @@ -290,7 +296,7 @@ pub mod pallet { era: EraNumber, amount: Balance, }, - /// Bonus reward has been paid out to a loyal staker. + /// Bonus reward has been paid out to a 'loyal' staker. BonusReward { account: T::AccountId, smart_contract: T::SmartContract, @@ -429,7 +435,7 @@ pub mod pallet { T::AccountId, Blake2_128Concat, T::SmartContract, - SingularStakingInfo, + SingularStakingInfoFor, OptionQuery, >; @@ -1069,7 +1075,7 @@ pub mod pallet { // Entry exists but period doesn't match. Bonus reward might still be claimable. Some(staking_info) if staking_info.period_number() >= threshold_period - && staking_info.is_loyal() => + && staking_info.has_bonus() => { return Err(Error::::UnclaimedRewards.into()); } @@ -1391,8 +1397,8 @@ pub mod pallet { /// Cleanup expired stake entries for the contract. /// /// Entry is considered to be expired if: - /// 1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward. - /// 2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not. + /// 1. It's from a past period & the account has NO BONUS reward. + /// 2. It's from a period older than the oldest claimable period, regardless whether the account was has bonus reward or not. #[pallet::call_index(17)] #[pallet::weight(T::WeightInfo::cleanup_expired_entries( T::MaxNumberOfStakedContracts::get() @@ -1409,7 +1415,7 @@ pub mod pallet { // This is bounded by max allowed number of stake entries per account. let to_be_deleted: Vec = StakerInfo::::iter_prefix(&account) .filter_map(|(smart_contract, stake_info)| { - if stake_info.period_number() < current_period && !stake_info.is_loyal() + if stake_info.period_number() < current_period && !stake_info.has_bonus() || stake_info.period_number() < threshold_period { Some(smart_contract) @@ -2159,7 +2165,7 @@ pub mod pallet { // Ensure: // 1. Period for which rewards are being claimed has ended. - // 2. Account has been a loyal staker. + // 2. Account is eligible to bonus rewards. // 3. Rewards haven't expired. let staked_period = staker_info.period_number(); ensure!( @@ -2167,7 +2173,7 @@ pub mod pallet { Error::::NoClaimableRewards ); ensure!( - staker_info.is_loyal(), + staker_info.has_bonus(), Error::::NotEligibleForBonusReward ); ensure!( diff --git a/pallets/dapp-staking/src/test/mock.rs b/pallets/dapp-staking/src/test/mock.rs index 1a1b1532cd..a84f09e156 100644 --- a/pallets/dapp-staking/src/test/mock.rs +++ b/pallets/dapp-staking/src/test/mock.rs @@ -26,7 +26,9 @@ use frame_support::{ construct_runtime, derive_impl, migrations::MultiStepMigrator, ord_parameter_types, parameter_types, - traits::{fungible::Mutate as FunMutate, ConstBool, ConstU128, ConstU32, EitherOfDiverse}, + traits::{ + fungible::Mutate as FunMutate, ConstBool, ConstU128, ConstU32, ConstU8, EitherOfDiverse, + }, weights::Weight, }; use sp_arithmetic::fixed_point::FixedU128; @@ -258,6 +260,7 @@ impl pallet_dapp_staking::Config for Test { type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper; diff --git a/pallets/dapp-staking/src/test/testing_utils.rs b/pallets/dapp-staking/src/test/testing_utils.rs index 018593e6b3..c4c7168f97 100644 --- a/pallets/dapp-staking/src/test/testing_utils.rs +++ b/pallets/dapp-staking/src/test/testing_utils.rs @@ -19,9 +19,9 @@ use crate::test::mock::*; use crate::types::*; use crate::{ - pallet::Config, ActiveProtocolState, ContractStake, CurrentEraInfo, DAppId, DAppTiers, - EraRewards, Event, FreezeReason, HistoryCleanupMarker, IntegratedDApps, Ledger, NextDAppId, - PeriodEnd, PeriodEndInfo, StakerInfo, + pallet::Config, ActiveProtocolState, BonusStatusFor, ContractStake, CurrentEraInfo, DAppId, + DAppTiers, EraRewards, Event, FreezeReason, HistoryCleanupMarker, IntegratedDApps, Ledger, + NextDAppId, PeriodEnd, PeriodEndInfo, StakerInfo, }; use frame_support::{ @@ -54,7 +54,7 @@ pub(crate) struct MemorySnapshot { ::AccountId, ::SmartContract, ), - SingularStakingInfo, + SingularStakingInfo<::MaxBonusMovesPerPeriod>, >, contract_stake: HashMap, era_rewards: HashMap::EraRewardSpanLength>>, @@ -550,9 +550,10 @@ pub(crate) fn assert_stake( ); assert_eq!(post_staker_info.period_number(), stake_period); assert_eq!( - post_staker_info.is_loyal(), - pre_staker_info.is_loyal(), - "Staking operation mustn't change loyalty flag." + post_staker_info.has_bonus(), + pre_staker_info.has_bonus(), + "Staking operation mustn't change bonus reward + eligibility." ); } // A new entry is created. @@ -570,7 +571,7 @@ pub(crate) fn assert_stake( ); assert_eq!(post_staker_info.period_number(), stake_period); assert_eq!( - post_staker_info.is_loyal(), + post_staker_info.has_bonus(), stake_subperiod == Subperiod::Voting ); } @@ -713,20 +714,42 @@ pub(crate) fn assert_unstake( "Staked amount must decrease by the 'amount'" ); - let is_loyal = pre_staker_info.is_loyal() - && match unstake_subperiod { - Subperiod::Voting => !post_staker_info.staked_amount(Subperiod::Voting).is_zero(), - Subperiod::BuildAndEarn => { - post_staker_info.staked_amount(Subperiod::Voting) - == pre_staker_info.staked_amount(Subperiod::Voting) - } - }; + let should_keep_bonus = if pre_staker_info.has_bonus() { + match pre_staker_info.bonus_status { + BonusStatus::SafeMovesRemaining(remaining_moves) if remaining_moves > 0 => true, + _ => match unstake_subperiod { + Subperiod::Voting => { + !post_staker_info.staked_amount(Subperiod::Voting).is_zero() + } + Subperiod::BuildAndEarn => { + post_staker_info.staked_amount(Subperiod::Voting) + == pre_staker_info.staked_amount(Subperiod::Voting) + } + }, + } + } else { + false + }; assert_eq!( - post_staker_info.is_loyal(), - is_loyal, - "If 'Voting' stake amount is reduced in B&E period, loyalty flag must be set to false." + post_staker_info.has_bonus(), + should_keep_bonus, + "If 'voting stake' amount is fully unstaked in Voting subperiod or reduced in B&E subperiod, 'BonusStatus' must reflect this." ); + + if unstake_subperiod == Subperiod::BuildAndEarn + && pre_staker_info.has_bonus() + && post_staker_info.staked_amount(Subperiod::Voting) + < pre_staker_info.staked_amount(Subperiod::Voting) + { + let mut bonus_status_clone = pre_staker_info.bonus_status.clone(); + bonus_status_clone.decrease_moves(); + + assert_eq!( + post_staker_info.bonus_status, bonus_status_clone, + "'BonusStatus' must correctly decrease moves when 'voting stake' is reduced in B&E subperiod." + ); + } } let unstaked_amount_era_pairs = @@ -828,6 +851,24 @@ pub(crate) fn assert_unstake( } } +/// Assert the bonus status of a staker for a specific smart contract. +pub(crate) fn assert_bonus_status( + account: AccountId, + smart_contract: &MockSmartContract, + expected_bonus_status: BonusStatusFor, +) { + let snapshot = MemorySnapshot::new(); + let staker_info = snapshot + .staker_info + .get(&(account, *smart_contract)) + .expect("Staker info entry must exist to verify bonus status."); + + assert_eq!( + staker_info.bonus_status, expected_bonus_status, + "The staker's bonus status does not match the expected value." + ); +} + /// Claim staker rewards. pub(crate) fn assert_claim_staker_rewards(account: AccountId) { let pre_snapshot = MemorySnapshot::new(); @@ -1195,7 +1236,7 @@ pub(crate) fn assert_cleanup_expired_entries(account: AccountId) { .iter() .for_each(|((inner_account, contract), entry)| { if *inner_account == account { - if entry.period_number() < current_period && !entry.is_loyal() + if entry.period_number() < current_period && !entry.has_bonus() || entry.period_number() < threshold_period { to_be_deleted.push(contract); diff --git a/pallets/dapp-staking/src/test/tests.rs b/pallets/dapp-staking/src/test/tests.rs index f3014e326f..3f01012d6d 100644 --- a/pallets/dapp-staking/src/test/tests.rs +++ b/pallets/dapp-staking/src/test/tests.rs @@ -18,10 +18,10 @@ use crate::test::{mock::*, testing_utils::*}; use crate::{ - pallet::Config, ActiveProtocolState, ContractStake, DAppId, DAppTierRewardsFor, DAppTiers, - EraRewards, Error, Event, ForcingType, GenesisConfig, IntegratedDApps, Ledger, NextDAppId, - Perbill, PeriodNumber, Permill, Safeguard, StakerInfo, StaticTierParams, Subperiod, TierConfig, - TierThreshold, + pallet::Config, ActiveProtocolState, BonusStatus, ContractStake, DAppId, DAppTierRewardsFor, + DAppTiers, EraRewards, Error, Event, ForcingType, GenesisConfig, IntegratedDApps, Ledger, + NextDAppId, Perbill, PeriodNumber, Permill, Safeguard, StakerInfo, StaticTierParams, Subperiod, + TierConfig, TierThreshold, }; use frame_support::{ @@ -1246,7 +1246,7 @@ fn stake_fails_due_to_too_many_staked_contracts() { let account = 1; assert_lock(account, 100 as Balance * max_number_of_contracts as Balance); - // Advance to build&earn subperiod so we ensure non-loyal staking + // Advance to build&earn subperiod so we ensure 'non-loyal' staking advance_to_next_subperiod(); // Register smart contracts up to the max allowed number @@ -2147,10 +2147,10 @@ fn cleanup_expired_entries_is_ok() { // Scenario: // - 1st contract will be staked in the period that expires due to exceeded reward retention - // - 2nd contract will be staked in the period on the edge of expiry, with loyalty flag - // - 3rd contract will be be staked in the period on the edge of expiry, without loyalty flag - // - 4th contract will be staked in the period right before the current one, with loyalty flag - // - 5th contract will be staked in the period right before the current one, without loyalty flag + // - 2nd contract will be staked in the period on the edge of expiry, with bonus elegibility + // - 3rd contract will be be staked in the period on the edge of expiry, without bonus elegibility + // - 4th contract will be staked in the period right before the current one, with bonus elegibility + // - 5th contract will be staked in the period right before the current one, without bonus elegibility // // Expectation: 1, 3, 5 should be removed, 2 & 4 should remain @@ -2926,6 +2926,51 @@ fn stake_after_period_ends_with_max_staked_contracts() { }) } +#[test] +fn stake_after_period_ends_reset_bonus_status_is_ok() { + ExtBuilder::default().build_and_execute(|| { + let max_bonus_moves: u8 = ::MaxBonusMovesPerPeriod::get(); + + // Phase 1: Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::wasm(1 as AccountId); + assert_register(dev_account, &smart_contract); + + let account = 2; + let amount = 400; + let partial_unstake_amount = 100; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount - partial_unstake_amount); + + // Phase 2: Advance to B&E subperiod, we ensure 'bonus safe moves' remaining is decreased with a partial unstake (overflowing 'voting' stake) + advance_to_next_subperiod(); + assert_unstake(account, &smart_contract, partial_unstake_amount); + + if max_bonus_moves == 0 { + let expected_bonus_status = BonusStatus::BonusForfeited; + assert_bonus_status(account, &smart_contract, expected_bonus_status); + } else { + let expected_bonus_status = BonusStatus::SafeMovesRemaining(max_bonus_moves - 1); + assert_bonus_status(account, &smart_contract, expected_bonus_status); + } + + // Phase 3: Advance to the next period, claim rewards + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + if max_bonus_moves > 0 { + assert_claim_bonus_reward(account, &smart_contract); + } + + // Phase 4: Restake and verify BonusStatus reset + assert_stake(account, &smart_contract, partial_unstake_amount); + let default_bonus_status = BonusStatus::default(); + assert_bonus_status(account, &smart_contract, default_bonus_status); + }) +} + #[test] fn post_unlock_balance_cannot_be_transferred() { ExtBuilder::default().build_and_execute(|| { diff --git a/pallets/dapp-staking/src/test/tests_types.rs b/pallets/dapp-staking/src/test/tests_types.rs index 94ca8f0c09..eff1bb4a3c 100644 --- a/pallets/dapp-staking/src/test/tests_types.rs +++ b/pallets/dapp-staking/src/test/tests_types.rs @@ -39,6 +39,19 @@ macro_rules! get_u32_type { }; } +// Helper to generate custom `Get` types for testing the `BonusStatus` enum. +macro_rules! get_u8_type { + ($struct_name:ident, $value:expr) => { + #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] + struct $struct_name; + impl Get for $struct_name { + fn get() -> u8 { + $value + } + } + }; +} + #[test] fn subperiod_sanity_check() { assert_eq!(Subperiod::Voting.next(), Subperiod::BuildAndEarn); @@ -2010,21 +2023,175 @@ fn stake_amount_works() { assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); } +#[test] +fn default_bonus_status_works() { + get_u8_type!(MaxMoves, 2); + type TestBonusStatus = BonusStatus; + let default_value = TestBonusStatus::default(); + + assert_eq!( + default_value, + BonusStatus::SafeMovesRemaining(MaxMoves::get()), + "Default should use SafeMovesRemaining(MaxMoves) value" + ) +} + +#[test] +fn bonus_status_decrease_works() { + get_u8_type!(MaxMoves, 1); + type TestBonusStatus = BonusStatus; + let mut bonus_status = TestBonusStatus::default(); + + bonus_status.decrease_moves(); + assert_eq!( + bonus_status, + BonusStatus::SafeMovesRemaining(0), + "Safe moves must be decreased by one" + ); + + bonus_status.decrease_moves(); + assert_eq!( + bonus_status, + BonusStatus::BonusForfeited, + "Bonus must be forfeited" + ); + + // Decreasing one more time has no effet, bonus is already forfeited + bonus_status.decrease_moves(); + assert_eq!( + bonus_status, + BonusStatus::BonusForfeited, + "Bonus must be forfeited" + ); +} + +#[test] +fn singular_staking_encoding_decoding_works() { + get_u8_type!(MaxMoves, 2); + type TestSingularStakingInfo = SingularStakingInfo; + + let staking_info = TestSingularStakingInfo::new(0, Subperiod::Voting); + let encoded = staking_info.encode(); + let expected_encoded = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + 0x01, 0x02, // SafeMovesRemaining(2) + ]; + + assert_eq!(encoded, expected_encoded); + + let decoded: TestSingularStakingInfo = + Decode::decode(&mut &encoded[..]).expect("Decoding should succeed"); + + assert_eq!(decoded.bonus_status, BonusStatus::SafeMovesRemaining(2)); + + let staking_info_no_bonus = TestSingularStakingInfo::new(0, Subperiod::BuildAndEarn); + let encoded_no_bonus = staking_info_no_bonus.encode(); + let expected_encoded_no_bonus = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + 0x00, // BonusForfeited + ]; + + assert_eq!(encoded_no_bonus, expected_encoded_no_bonus); + + let decoded_no_bonus: TestSingularStakingInfo = + Decode::decode(&mut &encoded_no_bonus[..]).expect("Decoding should succeed"); + + assert_eq!(decoded_no_bonus.bonus_status, BonusStatus::BonusForfeited); +} + +#[test] +fn singular_staking_decoding_incorrect_moves_fails() { + get_u8_type!(MaxMoves, 5); + type TestSingularStakingInfo = SingularStakingInfo; + + let invalid_encoded_moves = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + 0x01, 0x06, // 6 moves (out of range for MaxMoves = 2) + ]; + + let decoded = TestSingularStakingInfo::decode(&mut &invalid_encoded_moves[..]); + assert!(decoded.is_err()); +} + +#[test] +fn legacy_singular_staking_format_decoding_works() { + get_u8_type!(MaxMoves, 2); + type TestSingularStakingInfo = SingularStakingInfo; + + let legacy_encoded_false: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + 0x00, // loyal_staker = false + ]; + + let decoded: TestSingularStakingInfo = + Decode::decode(&mut &legacy_encoded_false[..]).expect("Decoding should succeed"); + + assert_eq!(decoded.bonus_status, BonusStatus::BonusForfeited); + + let legacy_encoded_true: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + 0x01, // loyal_staker = true + ]; + + let decoded: TestSingularStakingInfo = + Decode::decode(&mut &legacy_encoded_true[..]).expect("Decoding should succeed"); + + assert_eq!(decoded.bonus_status, BonusStatus::SafeMovesRemaining(0)); + + let legacy_encoded_false_with_extra_byte: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + 0x00, // loyal_staker = false + 0x01, // useless extra byte + ]; + + let decoded: TestSingularStakingInfo = + Decode::decode(&mut &legacy_encoded_false_with_extra_byte[..]) + .expect("Decoding should succeed"); + + assert_eq!(decoded.bonus_status, BonusStatus::BonusForfeited); +} + +#[test] +fn legacy_singular_staking_incorrect_format_decoding_fails() { + get_u8_type!(MaxMoves, 2); + type TestSingularStakingInfo = SingularStakingInfo; + + let encoded_empty: Vec = vec![]; + let decoded = TestSingularStakingInfo::decode(&mut &encoded_empty[..]); + assert!(decoded.is_err()); + + let legacy_encoded_incomplete: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, // previous_staked (StakeAmount default) + 0x00, 0x00, 0x00, 0x00, // staked (StakeAmount default) + // missing byte + ]; + let decoded = TestSingularStakingInfo::decode(&mut &legacy_encoded_incomplete[..]); + assert!(decoded.is_err()); +} + #[test] fn singular_staking_info_basics_are_ok() { + get_u8_type!(MaxMoves, 0); + type TestSingularStakingInfo = SingularStakingInfo; let period_number = 3; let subperiod = Subperiod::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, subperiod); + let mut staking_info = TestSingularStakingInfo::new(period_number, subperiod); // Sanity checks assert_eq!(staking_info.period_number(), period_number); - assert!(staking_info.is_loyal()); + assert!(staking_info.has_bonus()); assert!(staking_info.total_staked_amount().is_zero()); assert!(staking_info.is_empty()); assert!(staking_info.era().is_zero()); - assert!(!SingularStakingInfo::new(period_number, Subperiod::BuildAndEarn).is_loyal()); + assert!(!TestSingularStakingInfo::new(period_number, Subperiod::BuildAndEarn).has_bonus()); - // Add some staked amount during `Voting` period + // Add some staked amount during `Voting` subperiod let era_1 = 7; let vote_stake_amount_1 = 11; staking_info.stake(vote_stake_amount_1, era_1, Subperiod::Voting); @@ -2070,16 +2237,19 @@ fn singular_staking_info_basics_are_ok() { #[test] fn singular_staking_info_unstake_during_voting_is_ok() { + get_u8_type!(MaxMoves, 1); + type TestSingularStakingInfo = SingularStakingInfo; let period_number = 3; let subperiod = Subperiod::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, subperiod); + let mut staking_info = TestSingularStakingInfo::new(period_number, subperiod); // Prep actions let era_1 = 2; let vote_stake_amount_1 = 11; staking_info.stake(vote_stake_amount_1, era_1, Subperiod::Voting); + let bonus_status_snapshot = staking_info.bonus_status; - // 1. Unstake some amount during `Voting` period, loyalty should remain as expected. + // 1. Unstake some amount during `Voting` period, bonus status should remain as expected. let unstake_amount_1 = 5; assert_eq!( staking_info.unstake(unstake_amount_1, era_1, Subperiod::Voting), @@ -2089,7 +2259,11 @@ fn singular_staking_info_unstake_during_voting_is_ok() { staking_info.total_staked_amount(), vote_stake_amount_1 - unstake_amount_1 ); - assert!(staking_info.is_loyal()); + assert_eq!( + staking_info.bonus_status, bonus_status_snapshot, + "Bonus should remain unchanged with max safe moves preserved." + ); + assert!(staking_info.bonus_status.has_bonus()); assert_eq!( staking_info.era(), era_1 + 1, @@ -2099,7 +2273,7 @@ fn singular_staking_info_unstake_during_voting_is_ok() { assert!(staking_info.previous_staked.is_empty()); assert!(staking_info.previous_staked.era.is_zero()); - // 2. Fully unstake, attempting to underflow, and ensure loyalty flag has been removed. + // 2. Fully unstake, attempting to underflow, and ensure bonus is forfeited. let era_2 = era_1 + 2; let remaining_stake = staking_info.total_staked_amount(); assert_eq!( @@ -2109,8 +2283,8 @@ fn singular_staking_info_unstake_during_voting_is_ok() { ); assert!(staking_info.total_staked_amount().is_zero()); assert!( - !staking_info.is_loyal(), - "Loyalty flag should have been removed since it was full unstake." + !staking_info.has_bonus(), + "Bonus should have been forfeited since it was full unstake." ); assert!(staking_info.era().is_zero()); @@ -2120,9 +2294,18 @@ fn singular_staking_info_unstake_during_voting_is_ok() { #[test] fn singular_staking_info_unstake_during_bep_is_ok() { + get_u8_type!(MaxMoves, 1); + type TestSingularStakingInfo = SingularStakingInfo; let period_number = 3; let subperiod = Subperiod::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, subperiod); + let mut staking_info = TestSingularStakingInfo::new(period_number, subperiod); + + // Sanity check + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(1), + "Sanity check to cover all scenarios.", + ); // Prep actions let era_1 = 3; @@ -2153,7 +2336,12 @@ fn singular_staking_info_unstake_during_bep_is_ok() { staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 - unstake_1 ); - assert!(staking_info.is_loyal()); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(1), + "Bonus should remain unchanged with max safe moves preserved." + ); + assert!(staking_info.has_bonus()); assert_eq!( staking_info.era(), era_1 + 1, @@ -2190,6 +2378,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 + bep_stake_amount_2 - unstake_1 - unstake_2 ); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(1), + "Bonus should remain unchanged with max safe moves preserved." + ); assert_eq!( staking_info.previous_staked.total(), @@ -2197,7 +2390,7 @@ fn singular_staking_info_unstake_during_bep_is_ok() { ); assert_eq!(staking_info.previous_staked.era, era_1); - // 3rd scenario - unstake all of the amount staked during B&E period, and then some more. + // 3rd scenario - unstake all of the amount staked during B&E subperiod, and then some more. // The point is to take a chunk from the voting subperiod stake too. let current_total_stake = staking_info.total_staked_amount(); let current_bep_stake = staking_info.staked_amount(Subperiod::BuildAndEarn); @@ -2210,6 +2403,13 @@ fn singular_staking_info_unstake_during_bep_is_ok() { vec![(era_2, unstake_2), (era_2 + 1, unstake_2)], "Also chipping away from the next era since the unstake is relevant to the ongoing era." ); + + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(0), + "Bonus status moves counter should have been decreased to 0." + ); + assert!(staking_info.has_bonus(), "Bonus should have been preserved since it is the first partial unstake from the 'voting subperiod' stake"); assert_eq!( staking_info.total_staked_amount(), current_total_stake - unstake_2 @@ -2221,27 +2421,45 @@ fn singular_staking_info_unstake_during_bep_is_ok() { assert!(staking_info .staked_amount(Subperiod::BuildAndEarn) .is_zero()); - assert!( - !staking_info.is_loyal(), - "Loyalty flag should have been removed due to non-zero voting subperiod unstake" - ); assert_eq!(staking_info.era(), era_2); assert_eq!(staking_info.previous_staked.total(), current_total_stake); assert_eq!(staking_info.previous_staked.era, era_2 - 1); + + // 4th scenario - Bonus forfeited + // Fully exhaust the bonus by performing another unstake during the B&E subperiod + let era_3 = era_2 + 2; + let unstake_3 = 5; + + assert_eq!( + staking_info.unstake(unstake_3, era_3, Subperiod::BuildAndEarn), + vec![(era_3, unstake_3), (era_3 + 1, unstake_3)] + ); + assert_eq!( + staking_info.bonus_status, + BonusStatus::BonusForfeited, + "Bonus should be forfeited after exhausting all safe moves." + ); + assert!( + !staking_info.has_bonus(), + "Bonus should no longer be active." + ); + assert_eq!(staking_info.era(), era_3); } #[test] fn singular_staking_info_unstake_era_amount_pairs_are_ok() { + get_u8_type!(MaxMoves, 1); + type TestSingularStakingInfo = SingularStakingInfo; let period_number = 1; let subperiod = Subperiod::BuildAndEarn; // 1. Unstake only reduces the amount from a the future era { - let era = 3; + let era: u32 = 3; let stake_amount = 13; let unstake_amount = 3; - let mut staking_info = SingularStakingInfo::new(period_number, subperiod); + let mut staking_info = TestSingularStakingInfo::new(period_number, subperiod); staking_info.stake(stake_amount, era, Subperiod::BuildAndEarn); assert_eq!( @@ -2255,7 +2473,7 @@ fn singular_staking_info_unstake_era_amount_pairs_are_ok() { let era = 3; let stake_amount = 17; let unstake_amount = 5; - let mut staking_info = SingularStakingInfo::new(period_number, subperiod); + let mut staking_info = TestSingularStakingInfo::new(period_number, subperiod); staking_info.stake(stake_amount, era, Subperiod::BuildAndEarn); assert_eq!( @@ -2272,7 +2490,7 @@ fn singular_staking_info_unstake_era_amount_pairs_are_ok() { let era = 3; let stake_amount = 17; let unstake_amount = 5; - let mut staking_info = SingularStakingInfo::new(period_number, subperiod); + let mut staking_info = TestSingularStakingInfo::new(period_number, subperiod); staking_info.stake(stake_amount, era, Subperiod::BuildAndEarn); assert_eq!( @@ -2284,6 +2502,54 @@ fn singular_staking_info_unstake_era_amount_pairs_are_ok() { } } +#[test] +fn bonus_status_transition_between_subperiods_is_ok() { + get_u8_type!(MaxMoves, 1); + type TestSingularStakingInfo = SingularStakingInfo; + let mut staking_info = TestSingularStakingInfo::new(1, Subperiod::Voting); + staking_info.staked.voting = 15; + + // Sanity check + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(1), + "Sanity check to cover all scenarios.", + ); + + // First unstake in Voting subperiod + staking_info.update_bonus_status(Subperiod::Voting, 10); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(1), + "Unstaking during Voting subperiod should not decrement safe moves if voting stake remains." + ); + + // Then unstake in B&E subperiod + staking_info.update_bonus_status(Subperiod::BuildAndEarn, 20); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(0), + "Safe moves should decrement when unstaking in B&E subperiod." + ); +} + +#[test] +fn bonus_status_no_change_on_exact_match() { + get_u8_type!(MaxMoves, 2); + type TestSingularStakingInfo = SingularStakingInfo; + + let mut staking_info = TestSingularStakingInfo::new(1, Subperiod::Voting); + staking_info.bonus_status = BonusStatus::SafeMovesRemaining(2); + staking_info.staked.voting = 15; + + staking_info.update_bonus_status(Subperiod::BuildAndEarn, 15); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(2), + "Safe moves should not decrement when voting stake matches the reference amount." + ); +} + #[test] fn contract_stake_amount_basic_get_checks_work() { // Sanity checks for empty struct diff --git a/pallets/dapp-staking/src/types.rs b/pallets/dapp-staking/src/types.rs index 8cad803274..dcaecaba4b 100644 --- a/pallets/dapp-staking/src/types.rs +++ b/pallets/dapp-staking/src/types.rs @@ -47,7 +47,7 @@ //! * `UnlockingChunk` - describes some amount undergoing the unlocking process. //! * `StakeAmount` - contains information about the staked amount in a particular era, and period. //! * `AccountLedger` - keeps track of total locked & staked balance, unlocking chunks and number of stake entries. -//! * `SingularStakingInfo` - contains information about a particular staker's stake on a specific smart contract. Used to track loyalty. +//! * `SingularStakingInfo` - contains information about a particular staker's stake on a specific smart contract. Used to track bonus reward elegibility. //! //! ## Era Information //! @@ -84,6 +84,12 @@ use crate::pallet::Config; // Convenience type for `AccountLedger` usage. pub type AccountLedgerFor = AccountLedger<::MaxUnlockingChunks>; +// Convenience type for `SingularStakingInfo` usage. +pub type SingularStakingInfoFor = SingularStakingInfo<::MaxBonusMovesPerPeriod>; + +// Convenience type for `BonusStatus` usage. +pub type BonusStatusFor = BonusStatus<::MaxBonusMovesPerPeriod>; + // Convenience type for `DAppTierRewards` usage. pub type DAppTierRewardsFor = DAppTierRewards<::MaxNumberOfContracts, ::NumberOfTiers>; @@ -993,20 +999,77 @@ impl EraInfo { } } +/// Bonus status to track remaining 'voting stake' safe move actions in the ongoing B&E subperiod. +/// Move actions during B&E refer either to: +/// - a 'partial unstake with voting stake decrease', +/// - a 'stake transfer between two contracts'. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, TypeInfo)] +#[scale_info(skip_type_params(MaxBonusMoves))] +pub enum BonusStatus> { + /// Bonus rewards are forfeited. + BonusForfeited, + /// Bonus rewards are preserved with a value which is the number of remaining 'safe moves' allowed in the ongoing B&E subperiod. + SafeMovesRemaining(u8), + #[codec(skip)] + _Phantom(PhantomData), +} + +impl> Default for BonusStatus { + fn default() -> Self { + let max = MaxBonusMoves::get(); + BonusStatus::SafeMovesRemaining(max) + } +} + +impl> BonusStatus { + /// Decrease the number of remaining safe moves by 1. + /// If the counter reaches 0, the bonus is forfeited. + pub fn decrease_moves(&mut self) { + *self = match self { + BonusStatus::SafeMovesRemaining(counter) => { + if *counter == 0 { + BonusStatus::BonusForfeited + } else { + BonusStatus::SafeMovesRemaining(counter.saturating_sub(1)) + } + } + _ => BonusStatus::BonusForfeited, + } + } + + /// Check if bonus rewards are preserved. + pub fn has_bonus(&self) -> bool { + matches!(self, BonusStatus::SafeMovesRemaining(_)) + } +} + +impl> PartialEq for BonusStatus { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (BonusStatus::BonusForfeited, BonusStatus::BonusForfeited) => true, + (BonusStatus::SafeMovesRemaining(a), BonusStatus::SafeMovesRemaining(b)) => a == b, + _ => false, + } + } +} + +impl> Eq for BonusStatus {} + /// Information about how much a particular staker staked on a particular smart contract. /// /// Keeps track of amount staked in the 'voting subperiod', as well as 'build&earn subperiod'. -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct SingularStakingInfo { +#[derive(Encode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +#[scale_info(skip_type_params(MaxBonusMoves))] +pub struct SingularStakingInfo> { /// Amount staked before, if anything. pub(crate) previous_staked: StakeAmount, /// Staked amount pub(crate) staked: StakeAmount, - /// Indicates whether a staker is a loyal staker or not. - pub(crate) loyal_staker: bool, + /// Tracks the remaining allowable actions to preserve a staking bonus, based on the staker's behavior during specific subperiods. + pub(crate) bonus_status: BonusStatus, } -impl SingularStakingInfo { +impl> SingularStakingInfo { /// Creates new instance of the struct. /// /// ## Args @@ -1014,14 +1077,20 @@ impl SingularStakingInfo { /// `period` - period number for which this entry is relevant. /// `subperiod` - subperiod during which this entry is created. pub(crate) fn new(period: PeriodNumber, subperiod: Subperiod) -> Self { + // Bonus staking is only possible if stake is first made during the voting subperiod. + let bonus_status = if subperiod == Subperiod::Voting { + BonusStatus::::default() + } else { + BonusStatus::::BonusForfeited + }; + Self { previous_staked: Default::default(), staked: StakeAmount { period, ..Default::default() }, - // Loyalty staking is only possible if stake is first made during the voting subperiod. - loyal_staker: subperiod == Subperiod::Voting, + bonus_status, } } @@ -1042,7 +1111,8 @@ impl SingularStakingInfo { /// Unstakes some of the specified amount from the contract. /// /// In case the `amount` being unstaked is larger than the amount staked in the `Voting` subperiod, - /// and `Voting` subperiod has passed, this will remove the _loyalty_ flag from the staker. + /// and `Voting` subperiod has passed, this will reduce the staker's remaining safe moves in the bonus status. + /// Once all safe moves are exhausted, the bonus will be forfeited. /// /// Returns a vector of `(era, amount)` pairs, where `era` is the era in which the unstake happened, /// and the amount is the corresponding amount. @@ -1067,12 +1137,8 @@ impl SingularStakingInfo { self.staked.era = self.staked.era.max(current_era); result.push((self.staked.era, unstaked_amount)); - // 2. Update loyal staker flag accordingly. - self.loyal_staker = self.loyal_staker - && match subperiod { - Subperiod::Voting => !self.staked.voting.is_zero(), - Subperiod::BuildAndEarn => self.staked.voting == staked_snapshot.voting, - }; + // 2. Update bonus status accordingly. + self.update_bonus_status(subperiod, staked_snapshot.voting); // 3. Determine what was the previous staked amount. // This is done by simply comparing where does the _previous era_ fit in the current context. @@ -1137,6 +1203,24 @@ impl SingularStakingInfo { result } + /// Updates the bonus_status based on the current subperiod + /// For Voting subperiod: bonus_status is forfeited for full unstake + /// For B&E: the number of 'bonus safe moves' remaining is reduced for full unstake or for partial unstake if it exceeds the previous ‘voting’ stake used as a reference (the bonus status changes to 'forfeited' if there are no safe moves remaining) + pub fn update_bonus_status(&mut self, subperiod: Subperiod, previous_voting_stake: Balance) { + match subperiod { + Subperiod::Voting => { + if self.staked.voting.is_zero() { + self.bonus_status = BonusStatus::BonusForfeited; + } + } + Subperiod::BuildAndEarn => { + if self.staked.voting < previous_voting_stake { + self.bonus_status.decrease_moves(); + } + } + } + } + /// Total staked on the contract by the user. Both subperiod stakes are included. pub fn total_staked_amount(&self) -> Balance { self.staked.total() @@ -1147,9 +1231,9 @@ impl SingularStakingInfo { self.staked.for_type(subperiod) } - /// If `true` staker has staked during voting subperiod and has never reduced their sta - pub fn is_loyal(&self) -> bool { - self.loyal_staker + /// If `true` staker has bonus rewards + pub fn has_bonus(&self) -> bool { + self.bonus_status.has_bonus() } /// Period for which this entry is relevant. @@ -1168,6 +1252,46 @@ impl SingularStakingInfo { } } +impl> Decode for SingularStakingInfo { + /// Decodes SingularStakingInfo from input, supporting both current and legacy format with 'loyal_staker' flag. + fn decode( + input: &mut I, + ) -> Result { + let previous_staked = StakeAmount::decode(input)?; + let staked = StakeAmount::decode(input)?; + + let bonus_status = match input.read_byte() { + Ok(0x00) => BonusStatus::BonusForfeited, // Legacy format: loyal_staker = false + Ok(0x01) => { + if input.remaining_len()?.unwrap_or(0) > 0 { + let remaining_moves = u8::decode(input)?; + if remaining_moves > MaxBonusMoves::get() { + return Err(parity_scale_codec::Error::from( + "Remaining bonus safe moves cannot exceed 'MaxBonusMoves' value.", + )); + } + + BonusStatus::SafeMovesRemaining(remaining_moves) + } else { + // Legacy format `loyal_staker = true` without count. + BonusStatus::SafeMovesRemaining(0) + } + } + _ => { + return Err(parity_scale_codec::Error::from( + "Invalid byte for BonusStatus", + )) + } + }; + + Ok(Self { + previous_staked, + staked, + bonus_status, + }) + } +} + /// Composite type that holds information about how much was staked on a contract in up to two distinct eras. /// /// This is needed since 'stake' operation only makes the staked amount valid from the next era. diff --git a/pallets/inflation/src/lib.rs b/pallets/inflation/src/lib.rs index c9baf754a0..29188fcd8a 100644 --- a/pallets/inflation/src/lib.rs +++ b/pallets/inflation/src/lib.rs @@ -494,7 +494,7 @@ pub struct InflationConfiguration { /// This is provided to the stakers according to formula: 'pool * min(1, total_staked / ideal_staked)'. #[codec(compact)] pub adjustable_staker_reward_pool_per_era: Balance, - /// Bonus reward pool per period, for loyal stakers. + /// Bonus reward pool per period, for eligible stakers. #[codec(compact)] pub bonus_reward_pool_per_period: Balance, /// The ideal staking rate, in respect to total issuance. diff --git a/precompiles/dapp-staking/src/lib.rs b/precompiles/dapp-staking/src/lib.rs index b60a8270ee..b83c2ec90a 100644 --- a/precompiles/dapp-staking/src/lib.rs +++ b/precompiles/dapp-staking/src/lib.rs @@ -266,7 +266,7 @@ where handle.record_db_read::( 24 + ProtocolState::max_encoded_len() + ::SmartContract::max_encoded_len() - + SingularStakingInfo::max_encoded_len(), + + SingularStakingInfo::<::MaxBonusMovesPerPeriod>::max_encoded_len(), )?; let smart_contract = @@ -402,7 +402,7 @@ where handle.record_db_read::( 24 + ProtocolState::max_encoded_len() + ::SmartContract::max_encoded_len() - + SingularStakingInfo::max_encoded_len(), + + SingularStakingInfo::<::MaxBonusMovesPerPeriod>::max_encoded_len(), )?; let smart_contract = @@ -527,7 +527,7 @@ where // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len handle.record_db_read::( 16 + ::SmartContract::max_encoded_len() - + SingularStakingInfo::max_encoded_len(), + + SingularStakingInfo::<::MaxBonusMovesPerPeriod>::max_encoded_len(), )?; let origin_smart_contract = @@ -688,7 +688,7 @@ where Ok(true) } - /// Attempts to claim bonus reward for being a loyal staker of the given dApp. + /// Attempts to claim bonus reward for an elegible staker. #[precompile::public("claim_bonus_reward((uint8,bytes))")] fn claim_bonus_reward( handle: &mut impl PrecompileHandle, diff --git a/precompiles/dapp-staking/src/test/mock.rs b/precompiles/dapp-staking/src/test/mock.rs index 04db72aa52..5b26246b4a 100644 --- a/precompiles/dapp-staking/src/test/mock.rs +++ b/precompiles/dapp-staking/src/test/mock.rs @@ -35,7 +35,7 @@ use sp_arithmetic::{fixed_point::FixedU128, Permill}; use sp_core::{H160, H256}; use sp_io::TestExternalities; use sp_runtime::{ - traits::{BlakeTwo256, ConstU32, IdentityLookup}, + traits::{BlakeTwo256, ConstU32, ConstU8, IdentityLookup}, BuildStorage, Perbill, }; extern crate alloc; @@ -279,6 +279,7 @@ impl pallet_dapp_staking::Config for Test { type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = pallet_dapp_staking::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper; diff --git a/runtime/astar/src/lib.rs b/runtime/astar/src/lib.rs index 61b6f7f780..5f76c74921 100644 --- a/runtime/astar/src/lib.rs +++ b/runtime/astar/src/lib.rs @@ -30,7 +30,7 @@ use frame_support::{ traits::{ fungible::{Balanced, Credit, HoldConsideration}, tokens::{PayFromAccount, UnityAssetBalanceConversion}, - AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, Contains, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Contains, EqualPrivilegeOnly, FindAuthor, Get, Imbalance, InstanceFilter, LinearStoragePrice, Nothing, OnFinalize, OnUnbalanced, Randomness, WithdrawReasons, }, @@ -462,6 +462,7 @@ impl pallet_dapp_staking::Config for Runtime { type MinimumStakeAmount = MinimumStakingAmount; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = weights::pallet_dapp_staking::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = DAppStakingBenchmarkHelper, AccountId>; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 7ce660b321..1a8330efa0 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -29,9 +29,9 @@ use frame_support::{ traits::{ fungible::{Balanced, Credit, HoldConsideration}, tokens::{PayFromAccount, UnityAssetBalanceConversion}, - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Contains, EqualPrivilegeOnly, - FindAuthor, Get, InsideBoth, InstanceFilter, LinearStoragePrice, Nothing, OnFinalize, - WithdrawReasons, + AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, ConstU8, Contains, + EqualPrivilegeOnly, FindAuthor, Get, InsideBoth, InstanceFilter, LinearStoragePrice, + Nothing, OnFinalize, WithdrawReasons, }, weights::{ constants::{ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -487,6 +487,7 @@ impl pallet_dapp_staking::Config for Runtime { type MinimumStakeAmount = ConstU128; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = pallet_dapp_staking::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper, AccountId>; diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index bf839ea052..9b6bb314e8 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -30,7 +30,7 @@ use frame_support::{ traits::{ fungible::{Balanced, Credit, HoldConsideration}, tokens::{PayFromAccount, UnityAssetBalanceConversion}, - AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, Contains, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Contains, EqualPrivilegeOnly, FindAuthor, Get, Imbalance, InsideBoth, InstanceFilter, LinearStoragePrice, Nothing, OnFinalize, OnUnbalanced, WithdrawReasons, }, @@ -494,6 +494,7 @@ impl pallet_dapp_staking::Config for Runtime { type MinimumStakeAmount = MinimumStakingAmount; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = weights::pallet_dapp_staking::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = DAppStakingBenchmarkHelper, AccountId>; diff --git a/runtime/shiden/src/lib.rs b/runtime/shiden/src/lib.rs index 80a6cf3ffe..8a96487aa6 100644 --- a/runtime/shiden/src/lib.rs +++ b/runtime/shiden/src/lib.rs @@ -29,8 +29,8 @@ use frame_support::{ genesis_builder_helper, parameter_types, traits::{ fungible::{Balanced, Credit}, - AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, Contains, FindAuthor, Get, Imbalance, - InstanceFilter, Nothing, OnFinalize, OnUnbalanced, WithdrawReasons, + AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, Contains, FindAuthor, Get, + Imbalance, InstanceFilter, Nothing, OnFinalize, OnUnbalanced, WithdrawReasons, }, weights::{ constants::{ @@ -450,6 +450,7 @@ impl pallet_dapp_staking::Config for Runtime { type MinimumStakeAmount = MinimumStakingAmount; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = weights::pallet_dapp_staking::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = DAppStakingBenchmarkHelper, AccountId>; diff --git a/tests/xcm-simulator/src/mocks/parachain.rs b/tests/xcm-simulator/src/mocks/parachain.rs index 26d2118936..ddb89405fb 100644 --- a/tests/xcm-simulator/src/mocks/parachain.rs +++ b/tests/xcm-simulator/src/mocks/parachain.rs @@ -23,8 +23,8 @@ use frame_support::{ dispatch::DispatchClass, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, Contains, Everything, - InstanceFilter, Nothing, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Contains, + Everything, InstanceFilter, Nothing, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -729,6 +729,7 @@ impl pallet_dapp_staking::Config for Runtime { type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; type RankingEnabled = ConstBool; + type MaxBonusMovesPerPeriod = ConstU8<2>; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper;