diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 2e0b479c0f..adedf470f0 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2258,6 +2258,42 @@ where Self::get_priority_staking(who, origin_hotkey, *alpha_amount), ) } + Some(Call::move_stake_limit { + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + }) => { + if ColdkeySwapScheduled::::contains_key(who) { + return InvalidTransaction::Custom( + CustomTransactionError::ColdkeyInSwapSchedule.into(), + ) + .into(); + } + + // Get the max amount possible to exchange + let max_amount = Pallet::::get_max_amount_move(*origin_netuid, *destination_netuid, *limit_price); + + // Fully validate the user input + Self::result_to_validity( + Pallet::::validate_stake_transition( + who, + who, + origin_hotkey, + destination_hotkey, + *origin_netuid, + *destination_netuid, + *alpha_amount, + max_amount, + Some(*allow_partial), + false, + ), + Self::get_priority_staking(who, origin_hotkey, *alpha_amount), + ) + } Some(Call::transfer_stake { destination_coldkey, hotkey, diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 98b83791e8..101c0d7594 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1865,6 +1865,53 @@ mod dispatches { ) } + /// Moves specified amount of stake from a hotkey to another across subnets with a price limit. + /// + /// # Arguments + /// * `origin` - (::Origin): - The signature of the caller's coldkey. + /// * `origin_hotkey` (T::AccountId): - The hotkey account to move stake from. + /// * `destination_hotkey` (T::AccountId): - The hotkey account to move stake to. + /// * `origin_netuid` (T::AccountId): - The subnet ID to move stake from. + /// * `destination_netuid` (T::AccountId): - The subnet ID to move stake to. + /// * `alpha_amount` (T::AccountId): - The alpha stake amount to move. + /// * `limit_price` (u64): - The limit price expressed in units of RAO per one Alpha. + /// * `allow_partial` (bool): - Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. + /// + /// # Errors + /// Returns an error if: + /// * The origin is not signed by the correct coldkey. + /// * Either subnet does not exist. + /// * The hotkeys do not exist. + /// * There is insufficient stake on `(coldkey, origin_hotkey, origin_netuid)`. + /// * The price is worse than the limit_price. + /// * The transfer amount is below the minimum stake requirement and partial execution is not allowed. + /// + /// # Events + /// Emits a `StakeMoved` event upon successful completion of the stake movement. + #[pallet::call_index(94)] + #[pallet::weight((Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No))] + pub fn move_stake_limit( + origin: T::RuntimeOrigin, + origin_hotkey: T::AccountId, + destination_hotkey: T::AccountId, + origin_netuid: u16, + destination_netuid: u16, + alpha_amount: u64, + limit_price: u64, + allow_partial: bool, + ) -> DispatchResult { + Self::do_move_stake_limit( + origin, + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ) + } + /// Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. /// /// # Arguments diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index 4198d29efc..d6dd7a859a 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -72,6 +72,81 @@ impl Pallet { Ok(()) } + /// Moves stake from one hotkey to another across subnets with a price limit. + /// + /// # Arguments + /// * `origin` - The origin of the transaction, which must be signed by the `origin_hotkey`. + /// * `origin_hotkey` - The account ID of the hotkey from which the stake is being moved. + /// * `destination_hotkey` - The account ID of the hotkey to which the stake is being moved. + /// * `origin_netuid` - The network ID of the origin subnet. + /// * `destination_netuid` - The network ID of the destination subnet. + /// * `alpha_amount` - The amount of stake to move. + /// * `limit_price` - The limit price expressed in units of RAO per one Alpha. + /// * `allow_partial` - Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. + /// + /// # Returns + /// * `DispatchResult` - Indicates the success or failure of the operation. + /// + /// # Errors + /// This function will return an error if: + /// * The origin is not signed by the `origin_hotkey`. + /// * Either the origin or destination subnet does not exist. + /// * The `origin_hotkey` or `destination_hotkey` does not exist. + /// * There are locked funds that cannot be moved across subnets. + /// * The price is worse than the limit_price. + /// * The transfer amount is below the minimum stake requirement and partial execution is not allowed. + /// + /// # Events + /// Emits a `StakeMoved` event upon successful completion of the stake movement. + pub fn do_move_stake_limit( + origin: T::RuntimeOrigin, + origin_hotkey: T::AccountId, + destination_hotkey: T::AccountId, + origin_netuid: u16, + destination_netuid: u16, + alpha_amount: u64, + limit_price: u64, + allow_partial: bool, + ) -> dispatch::DispatchResult { + // Check that the origin is signed by the origin_hotkey. + let coldkey = ensure_signed(origin)?; + + // Validate input and move stake + let tao_moved = Self::transition_stake_internal( + &coldkey, + &coldkey, + &origin_hotkey, + &destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + Some(limit_price), + Some(allow_partial), + false, + )?; + + // Log the event. + log::debug!( + "StakeMovedLimit( coldkey:{:?}, origin_hotkey:{:?}, origin_netuid:{:?}, destination_hotkey:{:?}, destination_netuid:{:?} )", + coldkey.clone(), + origin_hotkey.clone(), + origin_netuid, + destination_hotkey.clone(), + destination_netuid + ); + Self::deposit_event(Event::StakeMoved( + coldkey, + origin_hotkey, + origin_netuid, + destination_hotkey, + destination_netuid, + tao_moved, + )); + + // Ok and return. + Ok(()) + } + /// Transfers stake from one coldkey to another, optionally moving from one subnet to another, /// while keeping the same hotkey. /// diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index bc64a740fd..0214e734de 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -3686,6 +3686,85 @@ fn test_remove_stake_limit_validate() { }); } +// cargo test --package pallet-subtensor --lib -- tests::staking::test_move_stake_limit_validate --exact --show-output +#[test] +fn test_move_stake_limit_validate() { + // Testing the signed extension validate function + // correctly filters the `move_stake_limit` transaction. + + new_test_ext(0).execute_with(|| { + let origin_hotkey = U256::from(533453); + let destination_hotkey = U256::from(533454); + let coldkey = U256::from(55453); + let stake_amount = 300_000_000_000; + let move_amount = 150_000_000_000; + + // Add origin network + let origin_netuid: u16 = add_dynamic_network(&origin_hotkey, &coldkey); + + // Add destination network + let destination_netuid: u16 = add_dynamic_network(&destination_hotkey, &coldkey); + + // Give the neuron some stake to move + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + origin_netuid, + stake_amount, + ); + + // Force-set alpha in and tao reserve for origin subnet to make price equal 1.5 + let origin_tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let origin_alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(origin_netuid, origin_tao_reserve.to_num::()); + SubnetAlphaIn::::insert(origin_netuid, origin_alpha_in.to_num::()); + + // Force-set alpha in and tao reserve for destination subnet to make price equal 3.0 + let destination_tao_reserve: U96F32 = U96F32::from_num(300_000_000_000_u64); + let destination_alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(destination_netuid, destination_tao_reserve.to_num::()); + SubnetAlphaIn::::insert(destination_netuid, destination_alpha_in.to_num::()); + + // Verify prices + let origin_price: U96F32 = + U96F32::from_num(SubtensorModule::get_alpha_price(origin_netuid)); + let destination_price: U96F32 = + U96F32::from_num(SubtensorModule::get_alpha_price(destination_netuid)); + assert_eq!(origin_price, U96F32::from_num(1.5)); + assert_eq!(destination_price, U96F32::from_num(3.0)); + + // The relative price of origin to destination is 0.5 (1.5/3.0) + // Set limit price so that it would fail due to slippage (e.g., 0.6 which is higher than 0.5) + let limit_price = 600_000_000; // 0.6 in RAO units + + // Move stake limit call + let call = RuntimeCall::SubtensorModule(SubtensorCall::move_stake_limit { + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount: move_amount, + limit_price, + allow_partial: false, + }); + + let info: DispatchInfo = + DispatchInfoOf::<::RuntimeCall>::default(); + + let extension = SubtensorSignedExtension::::new(); + // Submit to the signed extension validate function + let result = extension.validate(&coldkey, &call.clone(), &info, 10); + + // Should fail due to slippage + assert_err!( + result, + TransactionValidityError::Invalid(InvalidTransaction::Custom( + CustomTransactionError::SlippageTooHigh.into() + )) + ); + }); +} + #[test] fn test_stake_overflow() { new_test_ext(1).execute_with(|| { @@ -5150,6 +5229,161 @@ fn test_remove_stake_limit_fill_or_kill() { }); } +#[test] +fn test_move_stake_limit_ok() { + new_test_ext(1).execute_with(|| { + let origin_hotkey = U256::from(533453); + let destination_hotkey = U256::from(533454); + let coldkey = U256::from(55453); + let stake_amount = 900_000_000_000; // over the maximum + let fee = DefaultStakingFee::::get(); + + // Add origin network + let origin_netuid: u16 = add_dynamic_network(&origin_hotkey, &coldkey); + + // Add destination network + let destination_netuid: u16 = add_dynamic_network(&destination_hotkey, &coldkey); + + // Give the neuron some stake to move + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + origin_netuid, + stake_amount, + ); + + // Force-set alpha in and tao reserve for origin subnet to make price equal 1.5 + let origin_tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let origin_alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(origin_netuid, origin_tao_reserve.to_num::()); + SubnetAlphaIn::::insert(origin_netuid, origin_alpha_in.to_num::()); + + // Force-set alpha in and tao reserve for destination subnet to make price equal 3.0 + let destination_tao_reserve: U96F32 = U96F32::from_num(300_000_000_000_u64); + let destination_alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(destination_netuid, destination_tao_reserve.to_num::()); + SubnetAlphaIn::::insert(destination_netuid, destination_alpha_in.to_num::()); + + // Verify prices + let origin_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(origin_netuid)); + let destination_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(destination_netuid)); + assert_eq!(origin_price, U96F32::from_num(1.5)); + assert_eq!(destination_price, U96F32::from_num(3.0)); + + // Setup limit price so that it doesn't exceed the slippage limit + // The amount that can be executed at this price is 450 TAO only + // Alpha produced will be calculated accordingly + let limit_price = 400_000_000; + let expected_executed_stake = 75_000_000_000; + + // Move stake limit call + assert_ok!(SubtensorModule::move_stake_limit( + RuntimeOrigin::signed(coldkey), + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + stake_amount, + limit_price, + true + )); + + // Check if stake has increased only by expected amount minus fee + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &coldkey, + destination_netuid + ), + expected_executed_stake - fee, + epsilon = expected_executed_stake / 1000, + ); + + // Check that price has updated appropriately + let exp_price = U96F32::from_num(6.0); + let current_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(destination_netuid)); + assert_abs_diff_eq!( + exp_price.to_num::(), + current_price.to_num::(), + epsilon = 0.0001, + ); + }); +} + +#[test] +fn test_move_stake_limit_fill_or_kill() { + new_test_ext(1).execute_with(|| { + let origin_hotkey = U256::from(533453); + let destination_hotkey = U256::from(533454); + let coldkey = U256::from(55453); + let stake_amount = 900_000_000_000; // over the maximum + let move_amount = 900_000_000_000; + + // Add origin network + let origin_netuid: u16 = add_dynamic_network(&origin_hotkey, &coldkey); + + // Add destination network + let destination_netuid: u16 = add_dynamic_network(&destination_hotkey, &coldkey); + + // Give the neuron some stake to move + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + origin_netuid, + stake_amount, + ); + + // Force-set alpha in and tao reserve for origin subnet to make price equal 1.5 + let origin_tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let origin_alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(origin_netuid, origin_tao_reserve.to_num::()); + SubnetAlphaIn::::insert(origin_netuid, origin_alpha_in.to_num::()); + + // Force-set alpha in and tao reserve for destination subnet to make price equal 3.0 + let destination_tao_reserve: U96F32 = U96F32::from_num(300_000_000_000_u64); + let destination_alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(destination_netuid, destination_tao_reserve.to_num::()); + SubnetAlphaIn::::insert(destination_netuid, destination_alpha_in.to_num::()); + + // Verify prices + let origin_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(origin_netuid)); + let destination_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(destination_netuid)); + assert_eq!(origin_price, U96F32::from_num(1.5)); + assert_eq!(destination_price, U96F32::from_num(3.0)); + + // Setup limit price + let limit_price = 400_000_000; + + // Move stake limit call with fill or kill set to false, should fail + assert_noop!( + SubtensorModule::move_stake_limit( + RuntimeOrigin::signed(coldkey), + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + move_amount, + limit_price, + false + ), + Error::::SlippageTooHigh + ); + + // Lower the amount and it should succeed now + let amount_ok = 45_000_000_000; // fits the maximum + assert_ok!(SubtensorModule::move_stake_limit( + RuntimeOrigin::signed(coldkey), + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + amount_ok, + limit_price, + false + )); + }); +} + // #[test] // fn test_add_stake_specific() { // new_test_ext(1).execute_with(|| { @@ -5467,7 +5701,8 @@ fn test_move_stake_limit_partial() { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); let coldkey = U256::from(1); - let hotkey = U256::from(2); + let origin_hotkey = U256::from(2); + let destination_hotkey = U256::from(3); let stake_amount = 150_000_000_000; let move_amount = 150_000_000_000; @@ -5475,18 +5710,18 @@ fn test_move_stake_limit_partial() { let origin_netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); let destination_netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - register_ok_neuron(origin_netuid, hotkey, coldkey, 192213123); - register_ok_neuron(destination_netuid, hotkey, coldkey, 192213123); + register_ok_neuron(origin_netuid, origin_hotkey, coldkey, 192213123); + register_ok_neuron(destination_netuid, destination_hotkey, coldkey, 192213123); - // Give the neuron some stake to remove + // Give the neuron some stake to move SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, + &origin_hotkey, &coldkey, origin_netuid, stake_amount, ); - // Forse-set alpha in and tao reserve to make price equal 1.5 on both origin and destination, + // Force-set alpha in and tao reserve to make price equal 1.5 on both origin and destination, // but there's much more liquidity on destination, so its price wouldn't go up when restaked let tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); @@ -5503,9 +5738,10 @@ fn test_move_stake_limit_partial() { let limit_price = 990_000_000; // Move stake with slippage safety - executes partially - assert_ok!(SubtensorModule::swap_stake_limit( + assert_ok!(SubtensorModule::move_stake_limit( RuntimeOrigin::signed(coldkey), - hotkey, + origin_hotkey, + destination_hotkey, origin_netuid, destination_netuid, move_amount, @@ -5513,13 +5749,21 @@ fn test_move_stake_limit_partial() { true, )); - let new_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, + // Check remaining balance in origin subnet + let origin_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, &coldkey, origin_netuid, ); + assert_abs_diff_eq!(origin_alpha, 149_000_000_000, epsilon = 100_000_000); - assert_abs_diff_eq!(new_alpha, 149_000_000_000, epsilon = 100_000_000,); + // Check moved balance in destination subnet + let destination_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &coldkey, + destination_netuid, + ); + assert_abs_diff_eq!(destination_alpha, 1_000_000_000, epsilon = 100_000_000); }); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a7a89608c9..acfeb30210 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -693,6 +693,7 @@ impl InstanceFilter for ProxyType { | RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake_limit { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake_limit { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::burned_register { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::root_register { .. })