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;