diff --git a/pallets/dapp-staking/src/lib.rs b/pallets/dapp-staking/src/lib.rs index 5c21ec210a..f1851d9ff8 100644 --- a/pallets/dapp-staking/src/lib.rs +++ b/pallets/dapp-staking/src/lib.rs @@ -318,11 +318,20 @@ 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, }, } @@ -403,6 +412,17 @@ pub mod pallet { 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, } /// General information about dApp staking protocol state. @@ -1518,106 +1538,103 @@ pub mod pallet { /// Used to claim bonus reward for a smart contract on behalf of the specified account, if eligible. #[pallet::call_index(21)] - #[pallet::weight(10_000)] - pub fn move_stake( - origin: OriginFor, - from_smart_contract: T::SmartContract, - to_smart_contract: T::SmartContract, - amount: Balance, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - // Ensure contracts are different and registered - ensure!( - from_smart_contract != to_smart_contract, - Error::::InvalidTargetContract - ); - ensure!( - Self::is_registered(&from_smart_contract), - Error::::ContractNotFound - ); - ensure!( - Self::is_registered(&to_smart_contract), - Error::::ContractNotFound - ); - - // Amount validation - ensure!(!amount.is_zero(), Error::::InvalidAmount); - - let protocol_state = ActiveProtocolState::::get(); - let current_era = protocol_state.era; - let period_info = protocol_state.period_info; - - // Reduce stake on source contract - StakerInfo::::try_mutate_exists( - &who, - &from_smart_contract, - |maybe_staker_info| -> DispatchResult { - let mut staker_info = - maybe_staker_info.take().ok_or(Error::::NoStakeFound)?; - ensure!( - staker_info.staked.total() >= amount, - Error::::InsufficientStakeAmount - ); + #[pallet::weight(T::WeightInfo::move_stake())] + pub fn move_stake( + origin: OriginFor, + from_smart_contract: T::SmartContract, + to_smart_contract: T::SmartContract, + amount: Balance, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!( + from_smart_contract != to_smart_contract, + Error::::InvalidTargetContract + ); + ensure!( + Self::is_registered(&from_smart_contract), + Error::::ContractNotFound + ); + ensure!( + Self::is_registered(&to_smart_contract), + Error::::ContractNotFound + ); + ensure!(!amount.is_zero(), Error::::InvalidAmount); + + let protocol_state = ActiveProtocolState::::get(); + let current_era = protocol_state.era; + let period_info = protocol_state.period_info; + + // Reduce stake on source contract + StakerInfo::::try_mutate_exists( + &who, + &from_smart_contract, + |maybe_staker_info| -> DispatchResult { + let mut staker_info = + maybe_staker_info.take().ok_or(Error::::NoStakeFound)?; + ensure!( + staker_info.staked.total() >= amount, + Error::::InsufficientStakeAmount + ); - staker_info.staked.subtract(amount); + staker_info.staked.subtract(amount); - // Update or remove the staking info - if staker_info.staked.total().is_zero() { - *maybe_staker_info = None; - } else { - *maybe_staker_info = Some(staker_info); + // Update or remove the staking info + if staker_info.staked.total().is_zero() { + *maybe_staker_info = None; + } else { + *maybe_staker_info = Some(staker_info); + } + Ok(()) + }, + )?; + + // Apply stake to target contract + StakerInfo::::try_mutate( + &who, + &to_smart_contract, + |maybe_staker_info| -> DispatchResult { + match maybe_staker_info { + Some(info) => { + // Add to existing stake + info.staked.add(amount, protocol_state.subperiod()); + info.bonus_status = BonusStatus::SafeMovesRemaining( + T::MaxBonusMovesPerPeriod::get().into() + ); } - Ok(()) - }, - )?; - - // Apply stake to target contract - StakerInfo::::try_mutate( - &who, - &to_smart_contract, - |maybe_staker_info| -> DispatchResult { - match maybe_staker_info { - Some(info) => { - // Add to existing stake - info.staked.add(amount, protocol_state.subperiod()); - info.bonus_status = BonusStatus::SafeMovesRemaining( - T::MaxBonusMovesPerPeriod::get().into(), - ); - } - None => { - // Create new stake entry - let mut new_info = SingularStakingInfo::new( - period_info.number, - protocol_state.subperiod(), - ); - new_info.staked.add(amount, protocol_state.subperiod()); - new_info.bonus_status = BonusStatus::SafeMovesRemaining( - T::MaxBonusMovesPerPeriod::get().into(), - ); - *maybe_staker_info = Some(new_info); - } + None => { + // Create new stake entry + let mut new_info = SingularStakingInfo::new( + period_info.number, + protocol_state.subperiod(), + ); + new_info.staked.add(amount, protocol_state.subperiod()); + new_info.bonus_status = BonusStatus::SafeMovesRemaining( + T::MaxBonusMovesPerPeriod::get().into() + ); + *maybe_staker_info = Some(new_info); } - Ok(()) - }, - )?; - - // Emit event - Self::deposit_event(Event::StakeMoved { - staker: who, - from_contract: from_smart_contract, - to_contract: to_smart_contract, - amount, - }); - - Ok(()) - } + } + Ok(()) + }, + )?; + + // Emit event + Self::deposit_event(Event::StakeMoved { + staker: who, + from_contract: from_smart_contract, + to_contract: to_smart_contract, + amount, + remaining_moves: T::MaxBonusMovesPerPeriod::get(), + }); + + Ok(()) + } } impl Pallet { pub fn is_registered(contract: &T::SmartContract) -> bool { - //TODO: Implement this - true + IntegratedDApps::::contains_key(contract) } /// `true` if the account is a staker, `false` otherwise. pub fn is_staker(account: &T::AccountId) -> bool { diff --git a/pallets/dapp-staking/src/test/tests.rs b/pallets/dapp-staking/src/test/tests.rs index f9368c4ef6..435c88a3d0 100644 --- a/pallets/dapp-staking/src/test/tests.rs +++ b/pallets/dapp-staking/src/test/tests.rs @@ -46,9 +46,9 @@ use astar_primitives::{ Balance, BlockNumber, }; -use std::collections::BTreeMap; +use crate::test::tests::MaxBonusMovesPerPeriod; use crate::PeriodEnd; - +use std::collections::BTreeMap; #[test] fn maintenances_mode_works() { @@ -3428,7 +3428,7 @@ fn claim_bonus_reward_for_works() { 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); @@ -3437,12 +3437,12 @@ fn claim_bonus_reward_for_works() { 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); @@ -3451,7 +3451,7 @@ fn claim_bonus_reward_for_works() { Subperiod::Voting, "Must start in voting period" ); - + // Stake during voting period let stake_amount = 93; assert_stake(staker_account, &smart_contract, stake_amount); @@ -3460,10 +3460,10 @@ fn claim_bonus_reward_for_works() { // 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 protocol_state = ActiveProtocolState::::get(); println!("Protocol state after period advance: {:?}", protocol_state); @@ -3475,7 +3475,7 @@ fn claim_bonus_reward_for_works() { // 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); @@ -3483,15 +3483,20 @@ fn claim_bonus_reward_for_works() { let (init_staker_balance, init_claimer_balance) = ( Balances::free_balance(&staker_account), Balances::free_balance(&dev_account), - ); - println!("Initial balances - staker: {}, claimer: {}", init_staker_balance, init_claimer_balance); + println!( + "Initial balances - staker: {}, claimer: {}", + init_staker_balance, init_claimer_balance + ); // Attempt bonus claim - println!("Attempting bonus claim for period {}", protocol_state.period_number() - 1); + println!( + "Attempting bonus claim for period {}", + protocol_state.period_number() - 1 + ); let result = DappStaking::claim_bonus_reward( RuntimeOrigin::signed(staker_account), - smart_contract.clone() + smart_contract.clone(), ); println!("Bonus claim result: {:?}", result); @@ -3499,10 +3504,7 @@ fn claim_bonus_reward_for_works() { // Verify double claim fails assert_noop!( - DappStaking::claim_bonus_reward( - RuntimeOrigin::signed(staker_account), - smart_contract - ), + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(staker_account), smart_contract), Error::::NoClaimableRewards ); @@ -3511,19 +3513,25 @@ fn claim_bonus_reward_for_works() { let bonus_event = events .iter() .rev() - .find(|e| matches!(&e.event, RuntimeEvent::DappStaking(Event::BonusReward { .. }))) + .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 { + 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), @@ -3532,4 +3540,169 @@ fn claim_bonus_reward_for_works() { ); } }) +} +#[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(|| { + // 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); + + // Lock and stake + assert_lock(staker, 300); + assert_stake(staker, &smart_contract_1, 100); + + let expected_moves = 5; // Default max moves + + // Move stake + assert_ok!(DappStaking::move_stake( + RuntimeOrigin::signed(staker), + smart_contract_1.clone(), + smart_contract_2.clone(), + 50 + )); + + // Verify event with expected remaining moves + System::assert_last_event(RuntimeEvent::DappStaking(Event::StakeMoved { + staker, + from_contract: smart_contract_1, + to_contract: smart_contract_2, + amount: 50, + remaining_moves: expected_moves, + })); + }); +} +#[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!(!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 + ); + }); } \ No newline at end of file diff --git a/pallets/dapp-staking/src/weights.rs b/pallets/dapp-staking/src/weights.rs index 31859fc357..57beeb0c20 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,9 @@ 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(10_000, 0) + } } // For backwards compatibility and tests @@ -903,4 +907,7 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + fn move_stake() -> Weight { + Weight::from_parts(10_000, 0) + } } 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/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/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)) + } }