From 0e070a71490e3d15b750ad033f5ec93096a7f8e0 Mon Sep 17 00:00:00 2001 From: Silvereau Date: Sun, 17 Nov 2024 12:44:12 -0500 Subject: [PATCH] feat(dapp-staking): Rework bonus rewards mechanism - Introduced move extrinsic for stake reallocation - Implemented bonus status tracking with SafeMovesRemaining - Configured logic for max bonus moves and forfeiture - Added unit tests to validate bonus rewards functionality - Added benchmarking and V8 to V9 migration logic - Added comprehensive documentation for stake movement - Updated bonus reward documentation with move conditions Part of dApp Staking bonus rewards mechanism rework #1379 --- pallets/dapp-staking/README.md | 38 +- pallets/dapp-staking/src/benchmarking/mod.rs | 58 ++ pallets/dapp-staking/src/lib.rs | 256 +++++-- pallets/dapp-staking/src/migration.rs | 87 ++- pallets/dapp-staking/src/test/mock.rs | 5 +- pallets/dapp-staking/src/test/tests.rs | 680 +++++++++++++++++- pallets/dapp-staking/src/types.rs | 72 +- pallets/dapp-staking/src/weights.rs | 14 + precompiles/dapp-staking/src/test/mock.rs | 4 + precompiles/dapp-staking/src/test/mod.rs | 1 + precompiles/dapp-staking/src/test/tests_v2.rs | 1 + precompiles/dapp-staking/src/test/tests_v3.rs | 1 + runtime/astar/src/lib.rs | 7 +- .../astar/src/weights/pallet_dapp_staking.rs | 9 + runtime/local/src/lib.rs | 9 +- runtime/shibuya/src/lib.rs | 8 +- .../src/weights/pallet_dapp_staking.rs | 9 + runtime/shiden/src/lib.rs | 9 +- .../shiden/src/weights/pallet_dapp_staking.rs | 9 + tests/xcm-simulator/src/mocks/parachain.rs | 5 +- 20 files changed, 1169 insertions(+), 113 deletions(-) diff --git a/pallets/dapp-staking/README.md b/pallets/dapp-staking/README.md index b78827acde..ab5332755e 100644 --- a/pallets/dapp-staking/README.md +++ b/pallets/dapp-staking/README.md @@ -37,7 +37,7 @@ When `Voting` subperiod starts, all _stakes_ are reset to **zero**. Projects participating in dApp staking are expected to market themselves to (re)attract stakers. Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it. -Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate. +Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards, so they are encouraged to participate. `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. @@ -143,6 +143,25 @@ 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. +#### Moving Stake Between Contracts + +During a period, stakers have the ability to move their staked tokens between different contracts. This feature allows for greater flexibility in managing stakes without having to unstake and re-stake tokens. Here are the key points about stake movement: + +* Each staker has a limited number of safe moves per period (`MaxBonusMovesPerPeriod`) +* Moving stake during the Build&Earn subperiod consumes one of these moves +* Moves during the Voting subperiod don't count against the move limit +* Once all safe moves are used, any additional moves will forfeit the bonus reward +* The target contract must be registered and active +* Cannot move stake between the same contract +* Cannot move more tokens than currently staked on the source contract +* Cannot move zero amount +* Move counter resets at period boundaries + +This feature is particularly useful for: +* Adjusting strategy during a period without unstaking +* Responding to changes in dApp performance or status +* Optimizing reward potential while maintaining bonus eligibility + #### Unstaking Tokens User can at any time decide to unstake staked tokens. There's no _unstaking_ process associated with this action. @@ -177,6 +196,11 @@ Rewards are calculated using a simple formula: `staker_reward_pool * staker_stak If staker staked on a dApp during the voting subperiod, and didn't reduce their staked amount below what was staked at the end of the voting subperiod, this makes them eligible for the bonus reward. +To maintain bonus reward eligibility when moving stake during the Build&Earn subperiod, stakers must: +* Have remaining safe moves available +* Not reduce their total staked amount below the voting period amount +* Only move stake between registered contracts + Bonus rewards need to be claimed per contract, unlike staker rewards. Bonus reward is calculated using a simple formula: `bonus_reward_pool * staker_voting_subperiod_stake / total_voting_subperiod_stake`. @@ -230,14 +254,4 @@ others will be left out. There is no strict rule which defines this behavior - i having a larger stake than the other dApp(s). Tehnically, at the moment, the dApp with the lower `dApp Id` will have the advantage over a dApp with the larger Id. -### Reward Expiry - -Unclaimed rewards aren't kept indefinitely in storage. Eventually, they expire. -Stakers & developers should make sure they claim those rewards before this happens. - -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 +### \ No newline at end of file diff --git a/pallets/dapp-staking/src/benchmarking/mod.rs b/pallets/dapp-staking/src/benchmarking/mod.rs index c8ec3ddef9..ac9fa56de7 100644 --- a/pallets/dapp-staking/src/benchmarking/mod.rs +++ b/pallets/dapp-staking/src/benchmarking/mod.rs @@ -48,7 +48,65 @@ mod benchmarks { assert_last_event::(Event::::MaintenanceMode { enabled: true }.into()); } + #[benchmark] + fn move_stake() { + initial_config::(); + + // Set up a staker account and required funds + let staker: T::AccountId = whitelisted_caller(); + let amount = T::MinimumLockedAmount::get() * 2; + T::BenchmarkHelper::set_balance(&staker, amount); + + // Register the source contract + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let from_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + from_contract.clone(), + )); + + // Register the destination contract + let to_contract = T::BenchmarkHelper::get_smart_contract(2); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + to_contract.clone(), + )); + + // Lock funds and stake on source contract + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + from_contract.clone(), + amount, + )); + // Move to build and earn period to ensure move operation has worst case complexity + force_advance_to_next_subperiod::(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check - we need to be in build&earn period." + ); + + let move_amount = amount / 2; + + #[extrinsic_call] + _( + RawOrigin::Signed(staker.clone()), + from_contract.clone(), + to_contract.clone(), + move_amount, + ); + + // Verify that an event was emitted + let last_events = dapp_staking_events::(); + assert!(!last_events.is_empty(), "No events found"); + } #[benchmark] fn register() { initial_config::(); diff --git a/pallets/dapp-staking/src/lib.rs b/pallets/dapp-staking/src/lib.rs index c82b163d29..2350e83dc7 100644 --- a/pallets/dapp-staking/src/lib.rs +++ b/pallets/dapp-staking/src/lib.rs @@ -94,7 +94,7 @@ pub mod pallet { use super::*; /// The current storage version. - pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(8); + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(9); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -210,6 +210,8 @@ pub mod pallet { /// Tier ranking enabled. #[pallet::constant] type RankingEnabled: Get; + #[pallet::constant] + type MaxBonusMovesPerPeriod: Get; /// Weight info for various calls & operations in the pallet. type WeightInfo: WeightInfo; @@ -316,6 +318,21 @@ pub mod pallet { ExpiredEntriesRemoved { account: T::AccountId, count: u16 }, /// Privileged origin has forced a new era and possibly a subperiod to start from next block. Force { forcing_type: ForcingType }, + /// Stake was moved between contracts + StakeMoved { + staker: T::AccountId, + from_contract: T::SmartContract, + to_contract: T::SmartContract, + amount: Balance, + remaining_moves: u8, + }, + + /// Move counter was reset at period boundary + MovesReset { + staker: T::AccountId, + contract: T::SmartContract, + new_count: u8, + }, } #[pallet::error] @@ -392,6 +409,21 @@ pub mod pallet { NoExpiredEntries, /// Force call is not allowed in production. ForceNotAllowed, + InvalidTargetContract, + InvalidAmount, + NoStakeFound, + /// No safe moves remaining for this period + NoSafeMovesRemaining, + + /// Cannot move stake between same contract + SameContract, + + /// Cannot move zero amount + ZeroMoveAmount, + + /// Cannot move more than currently staked + InsufficientStakedAmount, + BonusForfeited, } /// General information about dApp staking protocol state. @@ -499,6 +531,10 @@ pub mod pallet { /// chain-fork debugging if required. #[pallet::storage] pub type Safeguard = StorageValue<_, bool, ValueQuery, DefaultSafeguard>; + #[pallet::storage] + #[pallet::getter(fn remaining_moves)] + pub type RemainingMoves = + StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>; #[pallet::genesis_config] pub struct GenesisConfig { @@ -1336,22 +1372,27 @@ pub mod pallet { let account = ensure_signed(origin)?; ensure!( - !IntegratedDApps::::contains_key(&smart_contract), + !Self::is_registered(&smart_contract), Error::::ContractStillActive ); let protocol_state = ActiveProtocolState::::get(); let current_era = protocol_state.era; - // Extract total staked amount on the specified unregistered contract - let amount = match StakerInfo::::get(&account, &smart_contract) { + // Capture staking info and moves before unstaking + let (amount, current_moves) = match StakerInfo::::get(&account, &smart_contract) { Some(staking_info) => { ensure!( staking_info.period_number() == protocol_state.period_number(), Error::::UnstakeFromPastPeriod ); - staking_info.total_staked_amount() + let moves = match staking_info.bonus_status { + BonusStatus::SafeMovesRemaining(n) => Some(n), + _ => None, + }; + + (staking_info.total_staked_amount(), moves) } None => { return Err(Error::::NoStakingInfo.into()); @@ -1362,17 +1403,9 @@ pub mod pallet { let mut ledger = Ledger::::get(&account); ledger .unstake_amount(amount, current_era, protocol_state.period_info) - .map_err(|err| match err { - // These are all defensive checks, which should never fail since we already checked them above. - AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewards - } - _ => Error::::InternalUnstakeError, - })?; + .map_err(|_| Error::::InternalUnstakeError)?; // Update total staked amount for the next era. - // This means 'fake' stake total amount has been kept until now, even though contract was unregistered. - // Although strange, it's been requested to keep it like this from the team. CurrentEraInfo::::mutate(|era_info| { era_info.unstake_amount(amount); }); @@ -1381,6 +1414,11 @@ pub mod pallet { Self::update_ledger(&account, ledger)?; StakerInfo::::remove(&account, &smart_contract); + // Preserve moves count for future stake operations + if let Some(moves) = current_moves { + RemainingMoves::::insert(&account, moves); + } + Self::deposit_event(Event::::UnstakeFromUnregistered { account, smart_contract, @@ -1506,21 +1544,143 @@ pub mod pallet { } /// Used to claim bonus reward for a smart contract on behalf of the specified account, if eligible. - #[pallet::call_index(20)] - #[pallet::weight(T::WeightInfo::claim_bonus_reward())] - pub fn claim_bonus_reward_for( + #[pallet::call_index(21)] + #[pallet::weight(T::WeightInfo::move_stake())] + pub fn move_stake( origin: OriginFor, - account: T::AccountId, - smart_contract: T::SmartContract, + from_smart_contract: T::SmartContract, + to_smart_contract: T::SmartContract, + amount: Balance, ) -> DispatchResult { - Self::ensure_pallet_enabled()?; - ensure_signed(origin)?; + let who = ensure_signed(origin)?; - Self::internal_claim_bonus_reward_for(account, smart_contract) + ensure!( + from_smart_contract != to_smart_contract, + Error::::InvalidTargetContract + ); + ensure!(!amount.is_zero(), Error::::InvalidAmount); + + // First check if contracts exist + let from_registered = Self::is_registered(&from_smart_contract); + ensure!(from_registered, Error::::ContractNotFound); + + let to_registered = Self::is_registered(&to_smart_contract); + ensure!(to_registered, Error::::ContractNotFound); + + let protocol_state = ActiveProtocolState::::get(); + let current_era = protocol_state.era; + let period_info = protocol_state.period_info; + + // Handle source contract staking + let mut bonus_status = StakerInfo::::try_mutate_exists( + &who, + &from_smart_contract, + |maybe_staker_info| -> Result { + let mut staker_info = + maybe_staker_info.take().ok_or(Error::::NoStakeFound)?; + + ensure!( + staker_info.staked.total() >= amount, + Error::::InsufficientStakedAmount + ); + + let status = match staker_info.bonus_status { + BonusStatus::BonusForfeited => { + return Err(Error::::BonusForfeited.into()) + } + BonusStatus::NoBonus => BonusStatus::NoBonus, + BonusStatus::SafeMovesRemaining(n) => { + if protocol_state.subperiod() == Subperiod::BuildAndEarn { + if n <= 1 { + // Changed condition here - will forfeit when this move depletes remaining move + BonusStatus::BonusForfeited + } else { + BonusStatus::SafeMovesRemaining(n - 1) + } + } else { + BonusStatus::SafeMovesRemaining(n) + } + } + }; + + staker_info.staked.subtract(amount); + + if staker_info.staked.total().is_zero() { + *maybe_staker_info = None; + } else { + staker_info.bonus_status = status.clone(); + *maybe_staker_info = Some(staker_info); + } + + Ok(status) + }, + )?; + + // Apply stake to target contract + StakerInfo::::try_mutate( + &who, + &to_smart_contract, + |maybe_staker_info| -> Result<(), DispatchError> { + match maybe_staker_info { + Some(info) => { + info.staked.add(amount, protocol_state.subperiod()); + info.bonus_status = bonus_status.clone(); + } + None => { + let mut new_info = SingularStakingInfo::new( + period_info.number, + protocol_state.subperiod(), + ); + new_info.staked.add(amount, protocol_state.subperiod()); + new_info.bonus_status = bonus_status.clone(); + *maybe_staker_info = Some(new_info); + } + } + Ok(()) + }, + )?; + + // Update contract stake accounting + if let Some(from_info) = IntegratedDApps::::get(&from_smart_contract) { + ContractStake::::try_mutate( + from_info.id, + |stake| -> Result<(), DispatchError> { + stake.staked.subtract(amount); + Ok(()) + }, + )?; + } + + if let Some(to_info) = IntegratedDApps::::get(&to_smart_contract) { + ContractStake::::try_mutate(to_info.id, |stake| -> Result<(), DispatchError> { + stake.stake(amount, period_info, current_era); + Ok(()) + })?; + } + + // Get the remaining moves for the event + let remaining_moves = match bonus_status { + BonusStatus::SafeMovesRemaining(n) => n.try_into().unwrap_or(0), + BonusStatus::BonusForfeited => 0, + BonusStatus::NoBonus => 0, + }; + + Self::deposit_event(Event::StakeMoved { + staker: who, + from_contract: from_smart_contract, + to_contract: to_smart_contract, + amount, + remaining_moves, + }); + + Ok(()) } } impl Pallet { + pub fn is_registered(contract: &T::SmartContract) -> bool { + IntegratedDApps::::contains_key(contract) + } /// `true` if the account is a staker, `false` otherwise. pub fn is_staker(account: &T::AccountId) -> bool { Ledger::::contains_key(account) @@ -2150,43 +2310,44 @@ pub mod pallet { .into()) } - /// Internal function that executes the `claim_bonus_reward` logic for the specified account & smart contract. fn internal_claim_bonus_reward_for( account: T::AccountId, smart_contract: T::SmartContract, ) -> DispatchResult { let staker_info = StakerInfo::::get(&account, &smart_contract) .ok_or(Error::::NoClaimableRewards)?; - let protocol_state = ActiveProtocolState::::get(); - // Ensure: - // 1. Period for which rewards are being claimed has ended. - // 2. Account has been a loyal staker. - // 3. Rewards haven't expired. + let protocol_state = ActiveProtocolState::::get(); let staked_period = staker_info.period_number(); - ensure!( - staked_period < protocol_state.period_number(), - Error::::NoClaimableRewards - ); - ensure!( - staker_info.is_loyal(), - Error::::NotEligibleForBonusReward - ); - ensure!( - staker_info.period_number() - >= Self::oldest_claimable_period(protocol_state.period_number()), - Error::::RewardExpired - ); + let oldest_claimable_period = + Self::oldest_claimable_period(protocol_state.period_number()); + + log::debug!("Account: {:?}", account); + log::debug!("Smart Contract: {:?}", smart_contract); + log::debug!("Staked Period: {:?}", staked_period); + log::debug!("Oldest Claimable Period: {:?}", oldest_claimable_period); + + // Check if reward has expired + if staked_period < oldest_claimable_period { + return Err(Error::::RewardExpired.into()); + } + + // Check if loyalty is required and not met + if !staker_info.is_loyal() { + return Err(Error::::NotEligibleForBonusReward.into()); + } let period_end_info = - PeriodEnd::::get(&staked_period).ok_or(Error::::InternalClaimBonusError)?; - // Defensive check - we should never get this far in function if no voting period stake exists. - ensure!( - !period_end_info.total_vp_stake.is_zero(), - Error::::InternalClaimBonusError - ); + PeriodEnd::::get(&staked_period).ok_or(Error::::NoClaimableRewards)?; + + log::debug!("Period End Info: {:?}", period_end_info); + // Ensure rewards are non-zero let eligible_amount = staker_info.staked_amount(Subperiod::Voting); + if period_end_info.total_vp_stake.is_zero() || eligible_amount.is_zero() { + return Err(Error::::NoClaimableRewards.into()); + } + let bonus_reward = Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; @@ -2194,7 +2355,6 @@ pub mod pallet { T::StakingRewardHandler::payout_reward(&account, bonus_reward) .map_err(|_| Error::::RewardPayoutFailed)?; - // Cleanup entry since the reward has been claimed StakerInfo::::remove(&account, &smart_contract); Ledger::::mutate(&account, |ledger| { ledger.contract_stake_count.saturating_dec(); diff --git a/pallets/dapp-staking/src/migration.rs b/pallets/dapp-staking/src/migration.rs index 79fa7e858c..d22ad44e6d 100644 --- a/pallets/dapp-staking/src/migration.rs +++ b/pallets/dapp-staking/src/migration.rs @@ -23,7 +23,7 @@ use frame_support::{ traits::{OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade}, weights::WeightMeter, }; - +use frame_support::StorageDoubleMap; #[cfg(feature = "try-runtime")] use sp_std::vec::Vec; @@ -33,7 +33,13 @@ use sp_runtime::TryRuntimeError; /// Exports for versioned migration `type`s for this pallet. pub mod versioned_migrations { use super::*; - + pub type V8ToV9 = frame_support::migrations::VersionedMigration< + 8, + 9, + v9::VersionMigrateV8ToV9, + Pallet, + ::DbWeight, + >; /// Migration V6 to V7 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring /// the migration is only performed when on-chain version is 6. pub type V6ToV7 = frame_support::migrations::VersionedMigration< @@ -56,6 +62,83 @@ pub mod versioned_migrations { >; } +pub mod v9 { + use super::*; + use crate::{BonusStatus, Config, Pallet, StakeAmount}; + + #[derive(Encode, Decode, Clone, Debug)] + pub struct OldSingularStakingInfo { + pub(crate) previous_staked: StakeAmount, + pub(crate) staked: StakeAmount, + pub(crate) loyal_staker: bool, + } + + #[derive(Encode, Decode, Clone, Debug)] + pub struct NewSingularStakingInfo { + pub(crate) previous_staked: StakeAmount, + pub(crate) staked: StakeAmount, + pub(crate) bonus_status: BonusStatus, + } + + #[storage_alias] + pub type StakerInfo = StorageDoubleMap< + Pallet, + Blake2_128Concat, + ::AccountId, + Blake2_128Concat, + ::SmartContract, + NewSingularStakingInfo, // Changed to NewSingularStakingInfo + OptionQuery, + >; + + pub struct VersionMigrateV8ToV9(PhantomData); + + impl UncheckedOnRuntimeUpgrade for VersionMigrateV8ToV9 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::in_code_storage_version(); + + let mut translated = 0usize; + + StakerInfo::::translate(|_account, _contract, old_value: OldSingularStakingInfo| { + translated.saturating_inc(); + + let bonus_status = if old_value.loyal_staker { + BonusStatus::SafeMovesRemaining(T::MaxBonusMovesPerPeriod::get().into()) + } else { + BonusStatus::NoBonus + }; + + Some(NewSingularStakingInfo { + previous_staked: old_value.previous_staked, + staked: old_value.staked, + bonus_status, + }) + }); + + current.put::>(); + + log::info!("Upgraded {translated} StakerInfo entries to {current:?}"); + + T::DbWeight::get().reads_writes(1 + translated as u64, 1 + translated as u64) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), TryRuntimeError> { + ensure!( + Pallet::::on_chain_storage_version() >= 9, + "dapp-staking::migration::v9: Wrong storage version" + ); + + Ok(()) + } + } +} + // TierThreshold as percentage of the total issuance mod v8 { use super::*; diff --git a/pallets/dapp-staking/src/test/mock.rs b/pallets/dapp-staking/src/test/mock.rs index 1a1b1532cd..9a02ab17a1 100644 --- a/pallets/dapp-staking/src/test/mock.rs +++ b/pallets/dapp-staking/src/test/mock.rs @@ -227,7 +227,9 @@ ord_parameter_types! { pub const ContractUnregisterAccount: AccountId = 1779; pub const ManagerAccount: AccountId = 25711; } - +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} impl pallet_dapp_staking::Config for Test { type RuntimeEvent = RuntimeEvent; type RuntimeFreezeReason = RuntimeFreezeReason; @@ -261,6 +263,7 @@ impl pallet_dapp_staking::Config for Test { type WeightInfo = weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } pub struct ExtBuilder {} diff --git a/pallets/dapp-staking/src/test/tests.rs b/pallets/dapp-staking/src/test/tests.rs index c3ffcbe732..1cfd9476ea 100644 --- a/pallets/dapp-staking/src/test/tests.rs +++ b/pallets/dapp-staking/src/test/tests.rs @@ -46,8 +46,13 @@ use astar_primitives::{ Balance, BlockNumber, }; +use crate::test::tests::MaxBonusMovesPerPeriod; +use crate::BonusStatus; +use crate::PeriodEnd; +use crate::RemainingMoves; +use frame_system::Origin; +use sp_core::H160; use std::collections::BTreeMap; - #[test] fn maintenances_mode_works() { ExtBuilder::default().build_and_execute(|| { @@ -3422,46 +3427,679 @@ fn claim_staker_rewards_for_basic_example_is_ok() { #[test] fn claim_bonus_reward_for_works() { ExtBuilder::default().build_and_execute(|| { - // Register smart contract, lock&stake some amount + // Register smart contract let dev_account = 1; let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); + // Verify starting conditions + let protocol_state = ActiveProtocolState::::get(); + println!("Initial protocol state: {:?}", protocol_state); + assert_eq!( + protocol_state.subperiod(), + Subperiod::Voting, + "Must start in voting period" + ); + + // Lock and stake in first period during voting let staker_account = 2; let lock_amount = 300; assert_lock(staker_account, lock_amount); + + // Verify starting conditions + let protocol_state = ActiveProtocolState::::get(); + println!("Initial protocol state: {:?}", protocol_state); + assert_eq!( + protocol_state.subperiod(), + Subperiod::Voting, + "Must start in voting period" + ); + + // Stake during voting period let stake_amount = 93; assert_stake(staker_account, &smart_contract, stake_amount); + println!("Staked {} in voting period", stake_amount); - // Advance to the next period, and claim the bonus + // Get staking info before period advance + let staking_info = StakerInfo::::get(&staker_account, &smart_contract); + println!("Staking info after stake: {:?}", staking_info); + + // Complete this period advance_to_next_period(); - let claimer_account = 3; + + let protocol_state = ActiveProtocolState::::get(); + println!("Protocol state after period advance: {:?}", protocol_state); + + // Claim regular staking rewards first + for _ in 0..required_number_of_reward_claims(staker_account) { + assert_claim_staker_rewards(staker_account); + } + + // Get staking info before bonus claim + let staking_info = StakerInfo::::get(&staker_account, &smart_contract); + println!("Staking info before bonus claim: {:?}", staking_info); + + // Check if period end info exists + let period_end = PeriodEnd::::get(protocol_state.period_number() - 1); + println!("Period end info: {:?}", period_end); + let (init_staker_balance, init_claimer_balance) = ( Balances::free_balance(&staker_account), - Balances::free_balance(&claimer_account), + Balances::free_balance(&dev_account), + ); + println!( + "Initial balances - staker: {}, claimer: {}", + init_staker_balance, init_claimer_balance ); - assert_ok!(DappStaking::claim_bonus_reward_for( - RuntimeOrigin::signed(claimer_account), - staker_account, - smart_contract.clone() + // Attempt bonus claim + println!( + "Attempting bonus claim for period {}", + protocol_state.period_number() - 1 + ); + let result = DappStaking::claim_bonus_reward( + RuntimeOrigin::signed(staker_account), + smart_contract.clone(), + ); + println!("Bonus claim result: {:?}", result); + + assert_ok!(result); + + // Verify double claim fails + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(staker_account), smart_contract), + Error::::NoClaimableRewards + ); + + // Verify the event + let events = System::events(); + let bonus_event = events + .iter() + .rev() + .find(|e| { + matches!( + &e.event, + RuntimeEvent::DappStaking(Event::BonusReward { .. }) + ) + }) + .expect("BonusReward event should exist"); + + if let RuntimeEvent::DappStaking(Event::BonusReward { + account, + smart_contract: event_contract, + period, + amount, + }) = &bonus_event.event + { + assert_eq!(account, &staker_account); + assert_eq!(event_contract, &smart_contract); + assert_eq!(period, &(protocol_state.period_number() - 1)); + + // Verify balances changed correctly + assert_eq!( + Balances::free_balance(&staker_account), + init_staker_balance + amount, + "Staker balance should increase by bonus amount" + ); + } + }) +} +#[test] +fn move_stake_basic_errors_work() { + ExtBuilder::default().build_and_execute(|| { + // Setup + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + // Cannot move zero amount + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 0 + ), + Error::::InvalidAmount // Updated to match actual error + ); + + // Cannot move between same contract + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_1.clone(), + 100 + ), + Error::::InvalidTargetContract + ); + + // Cannot move without stake + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 100 + ), + Error::::NoStakeFound + ); + }); +} + +#[test] +fn move_stake_events_work() { + ExtBuilder::default().build_and_execute(|| { + let staker = 1; + let from_contract = MockSmartContract::wasm(1); + let to_contract = MockSmartContract::wasm(2); + let stake_amount = 50; + let total_amount = 300; + + // Register contracts and set up staking + assert_register(staker, &from_contract); + assert_register(staker, &to_contract); + assert_lock(staker, total_amount); + assert_stake(staker, &from_contract, 100); + + let staker_info = StakerInfo::::get(staker, from_contract.clone()); + assert!( + staker_info.is_some(), + "StakerInfo should exist after staking on from_contract" + ); + + // Move stake from one contract to another + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + from_contract.clone(), + to_contract.clone(), + stake_amount )); - System::assert_last_event(RuntimeEvent::DappStaking(Event::BonusReward { - account: staker_account, - period: ActiveProtocolState::::get().period_number() - 1, - smart_contract, - // for this simple test, entire bonus reward pool goes to the staker - amount: ::StakingRewardHandler::bonus_reward_pool(), + + let remaining_moves = DappStaking::remaining_moves(staker); + System::assert_last_event(RuntimeEvent::DappStaking(Event::StakeMoved { + staker, + from_contract, + to_contract, + amount: stake_amount, + remaining_moves: 3, })); + assert_eq!(remaining_moves, 0, "Remaining moves must decrement by 1."); + }); +} + +#[test] +fn is_registered_works() { + ExtBuilder::default().build_and_execute(|| { + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + // Not registered initially + assert!(!DappStaking::is_registered(&smart_contract_1)); + assert!(!DappStaking::is_registered(&smart_contract_2)); + + // Register contract 1 + assert_register(1, &smart_contract_1); + assert!(DappStaking::is_registered(&smart_contract_1)); + assert!(!DappStaking::is_registered(&smart_contract_2)); + + // Unregister contract 1 + assert_unregister(&smart_contract_1); assert!( - Balances::free_balance(&staker_account) > init_staker_balance, - "Balance must have increased due to the reward payout." + !DappStaking::is_registered(&smart_contract_1), + "Should be false after unregister" ); + + // Register contract 2 + assert_register(2, &smart_contract_2); + assert!(!DappStaking::is_registered(&smart_contract_1)); + assert!(DappStaking::is_registered(&smart_contract_2)); + }); +} + +#[test] +fn move_stake_respects_registration() { + ExtBuilder::default().build_and_execute(|| { + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + // Try to move between unregistered contracts + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 100 + ), + Error::::ContractNotFound + ); + + // Register first contract + assert_register(1, &smart_contract_1); + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + // Try to move to unregistered contract + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + ), + Error::::ContractNotFound + ); + + // Register second contract and verify move works + assert_register(1, &smart_contract_2); + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + )); + + // Unregister first contract + assert_unregister(&smart_contract_1); + + // Verify can't move from unregistered contract + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + ), + Error::::ContractNotFound + ); + }); +} +#[test] +fn move_counter_mechanics_work() { + ExtBuilder::default().build_and_execute(|| { + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + let smart_contract_3 = MockSmartContract::Wasm(3); + + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + assert_register(1, &smart_contract_3); + + // Initial setup during voting period + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + // Check initial bonus status + let staking_info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); assert_eq!( - init_claimer_balance, - Balances::free_balance(&claimer_account), - "Claimer balance must not change since reward is deposited to the staker." + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(3), // Initial value is 3 + "Initial safe moves should be 3" ); - }) + + // Moving during voting period doesn't consume moves + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + )); + + let staking_info = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(3), // Preserved during voting + "Moves should be preserved during voting period" + ); + + // Advance to Build&Earn where moves are counted + advance_to_next_subperiod(); + + // Moving during B&E should decrement counter + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_2.clone(), + smart_contract_3.clone(), + 25 + )); + + let staking_info = StakerInfo::::get(&staker, &smart_contract_3).unwrap(); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(2), // Decremented by 1 + "Move counter should decrement during Build&Earn period" + ); + + // Counter should reset on period change + advance_to_next_period(); + + let staking_info = StakerInfo::::get(&staker, &smart_contract_3).unwrap(); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(2), // Maintains last value + "Move counter should maintain value across period boundary" + ); + }); +} +#[test] +fn move_counter_decrements_properly() { + ExtBuilder::default().build_and_execute(|| { + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + // Setup initial stake + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + advance_to_next_subperiod(); // Move to Build&Earn + + // Initial check + let initial_info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!( + initial_info.bonus_status, + BonusStatus::SafeMovesRemaining(3) + ); + + // First move + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + )); + + let info = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + assert_eq!(info.bonus_status, BonusStatus::SafeMovesRemaining(2)); + + // Second move + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_2.clone(), + smart_contract_1.clone(), + 25 + )); + + let info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!(info.bonus_status, BonusStatus::SafeMovesRemaining(1)); + }); +} +#[test] +fn move_during_voting_period_preserves_bonus() { + ExtBuilder::default().build_and_execute(|| { + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + // Initial bonus status check + let initial_info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!( + initial_info.bonus_status, + BonusStatus::SafeMovesRemaining(3) + ); + + // Multiple moves during voting period + for _ in 0..3 { + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 20 + )); + + let info = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + assert_eq!(info.bonus_status, BonusStatus::SafeMovesRemaining(3)); + + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_2.clone(), + smart_contract_1.clone(), + 20 + )); + + let info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!(info.bonus_status, BonusStatus::SafeMovesRemaining(3)); + } + }); +} + +#[test] +fn new_period_resets_move_counter() { + ExtBuilder::default().build_and_execute(|| { + // Register both contracts first + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + // Initial setup during voting period + let staker = 1; + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + // During voting period, moves don't decrement counter + let pre_move = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!( + pre_move.bonus_status, + BonusStatus::SafeMovesRemaining(3), + "Should start with 3 moves" + ); + + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + )); + + let post_move = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + assert_eq!( + post_move.bonus_status, + BonusStatus::SafeMovesRemaining(3), + "Should not decrement during voting period" + ); + + // Advance to Build&Earn period where moves should be counted + advance_to_next_subperiod(); + + // Now moves should decrement + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_2.clone(), + smart_contract_1.clone(), + 25 + )); + + let build_earn_move = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!( + build_earn_move.bonus_status, + BonusStatus::SafeMovesRemaining(2), + "Move counter should decrease in Build&Earn period" + ); + }); +} + +#[test] +fn unregistered_dapp_move_preserves_bonus() { + ExtBuilder::default().build_and_execute(|| { + // Setup initial contracts + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + // Initial stake during voting period + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + // Check initial bonus status + let initial_info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!( + initial_info.bonus_status, + BonusStatus::SafeMovesRemaining(3), + "Should start with 3 moves" + ); + + // Move to B&E where moves are counted + advance_to_next_subperiod(); + + // Move some stake + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + )); + + let move_info = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + assert_eq!( + move_info.bonus_status, + BonusStatus::SafeMovesRemaining(2), + "Move counter should decrease after first move" + ); + + // Unregister and verify moves preserved + assert_unregister(&smart_contract_2); + + // Store current moves count + let pre_unstake_info = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + let current_moves = match pre_unstake_info.bonus_status { + BonusStatus::SafeMovesRemaining(n) => n, + _ => panic!("Expected SafeMovesRemaining"), + }; + + // Unstake from unregistered should preserve moves + assert_ok!(DappStaking::unstake_from_unregistered( + RuntimeOrigin::signed(staker), + smart_contract_2 + )); + + // Stake back to first contract + assert_stake(staker, &smart_contract_1, 50); + + // Verify moves were preserved + let final_info = StakerInfo::::get(&staker, &smart_contract_1).unwrap(); + assert_eq!( + final_info.bonus_status, + BonusStatus::SafeMovesRemaining(current_moves), + "Move counter should be preserved when unstaking from unregistered dApp" + ); + }); +} + +#[test] +fn test_bonus_status() { + ExtBuilder::default().build_and_execute(|| { + // Register contracts first + let staker = 1; + let from_smart_contract = MockSmartContract::Wasm(1); + let to_smart_contract = MockSmartContract::Wasm(2); + + assert_register(staker, &from_smart_contract); + assert_register(staker, &to_smart_contract); + + // Need to lock before staking + assert_lock(staker, 300); + + // Simulate staking on the original smart contract + let stake_amount = 100; + assert_stake(staker, &from_smart_contract, stake_amount); + + // We should be in voting period initially + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting, + ); + + // Advance to Build&Earn to test move counting + advance_to_next_subperiod(); + + // Simulate a move from one smart contract to another - should decrement counter + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + from_smart_contract.clone(), + to_smart_contract.clone(), + stake_amount + )); + + // Bonus status should show one less move after the stake move + let staking_info = StakerInfo::::get(&staker, &to_smart_contract).unwrap(); + assert_eq!( + staking_info.bonus_status, + BonusStatus::SafeMovesRemaining(2) + ); + }); +} + +#[test] +fn zero_moves_remaining_forfeits_bonus() { + ExtBuilder::default().build_and_execute(|| { + // Setup + let staker = 1; + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + // Move to Build&Earn where moves count + advance_to_next_subperiod(); + + // Make moves until bonus is forfeited + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 20 + )); + + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_2.clone(), + smart_contract_1.clone(), + 20 + )); + + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 20 + )); + + // Verify BonusForfeited status + let staking_info = StakerInfo::::get(&staker, &smart_contract_2).unwrap(); + assert_eq!(staking_info.bonus_status, BonusStatus::BonusForfeited); + + // Verify that any further moves fail with BonusForfeited error + assert_noop!( + DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_2.clone(), + smart_contract_1.clone(), + 20 + ), + Error::::BonusForfeited + ); + }); } diff --git a/pallets/dapp-staking/src/types.rs b/pallets/dapp-staking/src/types.rs index 8cad803274..9122710593 100644 --- a/pallets/dapp-staking/src/types.rs +++ b/pallets/dapp-staking/src/types.rs @@ -832,7 +832,9 @@ impl Iterator for EraStakePairIter { } /// Describes stake amount in an particular era/period. -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +#[derive( + Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default, PartialOrd, +)] pub struct StakeAmount { /// Amount of staked funds accounting for the voting subperiod. #[codec(compact)] @@ -996,14 +998,25 @@ impl EraInfo { /// 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)] +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] 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, + pub(crate) previous_staked: StakeAmount, // Amount staked before, if anything. + pub(crate) staked: StakeAmount, // Currently staked amount. + pub(crate) bonus_status: BonusStatus, // New field for tracking bonus status. +} + +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] + +pub enum BonusStatus { + SafeMovesRemaining(u32), + NoBonus, + BonusForfeited, +} + +impl Default for BonusStatus { + fn default() -> Self { + BonusStatus::NoBonus + } } impl SingularStakingInfo { @@ -1020,8 +1033,12 @@ impl SingularStakingInfo { period, ..Default::default() }, - // Loyalty staking is only possible if stake is first made during the voting subperiod. - loyal_staker: subperiod == Subperiod::Voting, + bonus_status: match subperiod { + // Start with safe moves during voting period + Subperiod::Voting => BonusStatus::SafeMovesRemaining(3), // Or whatever initial value + // No bonus during build & earn period + Subperiod::BuildAndEarn => BonusStatus::NoBonus, + }, } } @@ -1060,6 +1077,8 @@ impl SingularStakingInfo { ) -> Vec<(EraNumber, Balance)> { let mut result = Vec::new(); let staked_snapshot = self.staked; + // Track initial voting amount before unstake + let initial_voting = self.staked.voting; // 1. Modify 'current' staked amount, and update the result. self.staked.subtract(amount); @@ -1067,12 +1086,25 @@ 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, - }; + // Update bonus status based on refined rules: + // Now using match on copied values to avoid ownership issues + let current_status = self.bonus_status; + self.bonus_status = match (subperiod, current_status) { + // During voting period, maintain loyalty until full unstake + (Subperiod::Voting, status) => { + if self.staked.total().is_zero() { + BonusStatus::NoBonus + } else { + status + } + } + // During B&E period, lose loyalty if voting amount is reduced + (Subperiod::BuildAndEarn, _) if self.staked.voting < initial_voting => { + BonusStatus::NoBonus + } + // Otherwise keep current status + (_, status) => status, + }; // 3. Determine what was the previous staked amount. // This is done by simply comparing where does the _previous era_ fit in the current context. @@ -1147,11 +1179,6 @@ 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 - } - /// Period for which this entry is relevant. pub fn period_number(&self) -> PeriodNumber { self.staked.period @@ -1166,6 +1193,9 @@ impl SingularStakingInfo { pub fn is_empty(&self) -> bool { self.staked.is_empty() } + pub fn is_loyal(&self) -> bool { + matches!(self.bonus_status, BonusStatus::SafeMovesRemaining(_)) + } } /// Composite type that holds information about how much was staked on a contract in up to two distinct eras. diff --git a/pallets/dapp-staking/src/weights.rs b/pallets/dapp-staking/src/weights.rs index 31859fc357..010a60392d 100644 --- a/pallets/dapp-staking/src/weights.rs +++ b/pallets/dapp-staking/src/weights.rs @@ -74,6 +74,7 @@ pub trait WeightInfo { fn dapp_tier_assignment(x: u32, ) -> Weight; fn on_idle_cleanup() -> Weight; fn step() -> Weight; + fn move_stake() -> Weight; } /// Weights for pallet_dapp_staking using the Substrate node and recommended hardware. @@ -489,6 +490,11 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + fn move_stake() -> Weight { + Weight::from_parts(36_000, 378) // 36 microseconds from benchmark, 378 bytes proof size + .saturating_add(T::DbWeight::get().reads(6)) // 6 storage reads + .saturating_add(T::DbWeight::get().writes(4)) // 4 storage writes + } } // For backwards compatibility and tests @@ -903,4 +909,12 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + + + fn move_stake() -> Weight { + Weight::from_parts(36_000, 378) + .saturating_add(RocksDbWeight::get().reads(6)) + .saturating_add(RocksDbWeight::get().writes(4)) + } + } diff --git a/precompiles/dapp-staking/src/test/mock.rs b/precompiles/dapp-staking/src/test/mock.rs index 04db72aa52..e81eb8ea5d 100644 --- a/precompiles/dapp-staking/src/test/mock.rs +++ b/precompiles/dapp-staking/src/test/mock.rs @@ -253,6 +253,9 @@ impl pallet_dapp_staking::BenchmarkHelper parameter_types! { pub const BaseNativeCurrencyPrice: FixedU128 = FixedU128::from_rational(5, 100); } +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} impl pallet_dapp_staking::Config for Test { type RuntimeEvent = RuntimeEvent; @@ -282,6 +285,7 @@ impl pallet_dapp_staking::Config for Test { type WeightInfo = pallet_dapp_staking::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } construct_runtime!( diff --git a/precompiles/dapp-staking/src/test/mod.rs b/precompiles/dapp-staking/src/test/mod.rs index c002cf0e6c..07a78ec8e3 100644 --- a/precompiles/dapp-staking/src/test/mod.rs +++ b/precompiles/dapp-staking/src/test/mod.rs @@ -20,3 +20,4 @@ mod mock; mod tests_v2; mod tests_v3; mod types; +pub use mock::*; diff --git a/precompiles/dapp-staking/src/test/tests_v2.rs b/precompiles/dapp-staking/src/test/tests_v2.rs index 5e358f35f7..02da6f2420 100644 --- a/precompiles/dapp-staking/src/test/tests_v2.rs +++ b/precompiles/dapp-staking/src/test/tests_v2.rs @@ -17,6 +17,7 @@ // along with Astar. If not, see . extern crate alloc; +use super::*; use crate::{test::mock::*, *}; use frame_support::assert_ok; use frame_system::RawOrigin; diff --git a/precompiles/dapp-staking/src/test/tests_v3.rs b/precompiles/dapp-staking/src/test/tests_v3.rs index df2f420f5c..f2a3245250 100644 --- a/precompiles/dapp-staking/src/test/tests_v3.rs +++ b/precompiles/dapp-staking/src/test/tests_v3.rs @@ -17,6 +17,7 @@ // along with Astar. If not, see . extern crate alloc; +use super::*; use crate::{test::mock::*, *}; use frame_support::assert_ok; use frame_system::RawOrigin; diff --git a/runtime/astar/src/lib.rs b/runtime/astar/src/lib.rs index a0e52da847..6939e53a32 100644 --- a/runtime/astar/src/lib.rs +++ b/runtime/astar/src/lib.rs @@ -113,6 +113,7 @@ pub mod xcm_config; pub type AstarAssetLocationIdConverter = AssetLocationIdConverter; pub use precompiles::{AstarPrecompiles, ASSET_PRECOMPILE_ADDRESS_PREFIX}; +use pallet_dapp_staking::migration::versioned_migrations; pub type Precompiles = AstarPrecompiles; use chain_extensions::AstarChainExtensions; @@ -389,6 +390,9 @@ impl DappStakingAccountCheck for AccountCheck { !CollatorSelection::is_account_candidate(account) } } +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} impl pallet_dapp_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; @@ -418,6 +422,7 @@ impl pallet_dapp_staking::Config for Runtime { type WeightInfo = weights::pallet_dapp_staking::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = DAppStakingBenchmarkHelper, AccountId>; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } pub struct InflationPayoutPerBlock; @@ -1338,7 +1343,7 @@ parameter_types! { pub type Migrations = (Unreleased, Permanent); /// Unreleased migrations. Add new ones here: -pub type Unreleased = (cumulus_pallet_xcmp_queue::migration::v5::MigrateV4ToV5,); +pub type Unreleased = (versioned_migrations::V8ToV9,); /// Migrations/checks that do not need to be versioned and can run on every upgrade. pub type Permanent = (pallet_xcm::migration::MigrateToLatestXcmVersion,); diff --git a/runtime/astar/src/weights/pallet_dapp_staking.rs b/runtime/astar/src/weights/pallet_dapp_staking.rs index 29b26bbffc..27ecb3aaf1 100644 --- a/runtime/astar/src/weights/pallet_dapp_staking.rs +++ b/runtime/astar/src/weights/pallet_dapp_staking.rs @@ -478,4 +478,13 @@ impl WeightInfo for SubstrateWeight { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + fn move_stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `6560` + // Minimum execution time: 10_060_000 picoseconds. + Weight::from_parts(10_314_000, 6560) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 185347545e..977d9b0f3f 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -96,7 +96,7 @@ pub use pallet_timestamp::Call as TimestampCall; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; - +use pallet_dapp_staking::migration::versioned_migrations; #[cfg(feature = "std")] /// Wasm binary unwrapped. If built with `BUILD_DUMMY_WASM_BINARY`, the function panics. pub fn wasm_binary_unwrap() -> &'static [u8] { @@ -462,6 +462,10 @@ parameter_types! { pub const BaseNativeCurrencyPrice: FixedU128 = FixedU128::from_rational(5, 100); } +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} + impl pallet_dapp_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeFreezeReason = RuntimeFreezeReason; @@ -490,6 +494,7 @@ impl pallet_dapp_staking::Config for Runtime { type WeightInfo = pallet_dapp_staking::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper, AccountId>; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } pub struct InflationPayoutPerBlock; @@ -1208,7 +1213,7 @@ pub type Executive = frame_executive::Executive< Migrations, >; -pub type Migrations = (); +pub type Migrations = (versioned_migrations::V8ToV9,); type EventRecord = frame_system::EventRecord< ::RuntimeEvent, diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index ada15891dd..d67fdf4641 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -115,7 +115,7 @@ use parachains_common::message_queue::NarrowOriginToSibling; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; - +use pallet_dapp_staking::migration::versioned_migrations; mod chain_extensions; pub mod genesis_config; mod precompiles; @@ -460,6 +460,9 @@ parameter_types! { pub const BaseNativeCurrencyPrice: FixedU128 = FixedU128::from_rational(5, 100); } +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} impl pallet_dapp_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeFreezeReason = RuntimeFreezeReason; @@ -488,6 +491,7 @@ impl pallet_dapp_staking::Config for Runtime { type WeightInfo = weights::pallet_dapp_staking::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = DAppStakingBenchmarkHelper, AccountId>; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } pub struct InflationPayoutPerBlock; @@ -1655,7 +1659,7 @@ pub type Executive = frame_executive::Executive< pub type Migrations = (Unreleased, Permanent); /// Unreleased migrations. Add new ones here: -pub type Unreleased = (); +pub type Unreleased = (versioned_migrations::V8ToV9,); /// Migrations/checks that do not need to be versioned and can run on every upgrade. pub type Permanent = (pallet_xcm::migration::MigrateToLatestXcmVersion,); diff --git a/runtime/shibuya/src/weights/pallet_dapp_staking.rs b/runtime/shibuya/src/weights/pallet_dapp_staking.rs index e10cd8d18c..bf405a0003 100644 --- a/runtime/shibuya/src/weights/pallet_dapp_staking.rs +++ b/runtime/shibuya/src/weights/pallet_dapp_staking.rs @@ -478,4 +478,13 @@ impl WeightInfo for SubstrateWeight { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + fn move_stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `6560` + // Minimum execution time: 10_060_000 picoseconds. + Weight::from_parts(10_314_000, 6560) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/runtime/shiden/src/lib.rs b/runtime/shiden/src/lib.rs index 754e752a8e..6b061a54d5 100644 --- a/runtime/shiden/src/lib.rs +++ b/runtime/shiden/src/lib.rs @@ -125,7 +125,7 @@ pub const MILLISDN: Balance = 1_000 * MICROSDN; pub const SDN: Balance = 1_000 * MILLISDN; pub const STORAGE_BYTE_FEE: Balance = 200 * NANOSDN; - +use pallet_dapp_staking::migration::versioned_migrations; /// Charge fee for stored bytes and items. pub const fn deposit(items: u32, bytes: u32) -> Balance { items as Balance * MILLISDN + (bytes as Balance) * STORAGE_BYTE_FEE @@ -425,6 +425,10 @@ parameter_types! { pub const BaseNativeCurrencyPrice: FixedU128 = FixedU128::from_rational(5, 100); } +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} + impl pallet_dapp_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeFreezeReason = RuntimeFreezeReason; @@ -453,6 +457,7 @@ impl pallet_dapp_staking::Config for Runtime { type WeightInfo = weights::pallet_dapp_staking::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = DAppStakingBenchmarkHelper, AccountId>; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } pub struct InflationPayoutPerBlock; @@ -1333,7 +1338,7 @@ parameter_types! { pub type Migrations = (Unreleased, Permanent); /// Unreleased migrations. Add new ones here: -pub type Unreleased = (); +pub type Unreleased = (versioned_migrations::V8ToV9,); /// Migrations/checks that do not need to be versioned and can run on every upgrade. pub type Permanent = (pallet_xcm::migration::MigrateToLatestXcmVersion,); diff --git a/runtime/shiden/src/weights/pallet_dapp_staking.rs b/runtime/shiden/src/weights/pallet_dapp_staking.rs index 169284360b..2b1de860ce 100644 --- a/runtime/shiden/src/weights/pallet_dapp_staking.rs +++ b/runtime/shiden/src/weights/pallet_dapp_staking.rs @@ -478,4 +478,13 @@ impl WeightInfo for SubstrateWeight { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + fn move_stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `6560` + // Minimum execution time: 10_060_000 picoseconds. + Weight::from_parts(10_314_000, 6560) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/tests/xcm-simulator/src/mocks/parachain.rs b/tests/xcm-simulator/src/mocks/parachain.rs index 26d2118936..bdf68180a7 100644 --- a/tests/xcm-simulator/src/mocks/parachain.rs +++ b/tests/xcm-simulator/src/mocks/parachain.rs @@ -703,7 +703,9 @@ impl pallet_dapp_staking::BenchmarkHelper parameter_types! { pub const BaseNativeCurrencyPrice: FixedU128 = FixedU128::from_rational(5, 100); } - +parameter_types! { + pub const MaxBonusMovesPerPeriod: u8 = 5; +} impl pallet_dapp_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeFreezeReason = RuntimeFreezeReason; @@ -732,6 +734,7 @@ impl pallet_dapp_staking::Config for Runtime { type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = BenchmarkHelper; + type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod; } type Block = frame_system::mocking::MockBlock;