From c8897391ae18250169af839d8eb4e5a52794b052 Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Tue, 16 Dec 2025 16:51:45 +0200 Subject: [PATCH 1/2] feat: add destination_hotkey parameter to transfer_stake Closes #1377 This PR allows users to specify a destination hotkey when transferring stake to a different coldkey. Previously, transfer_stake only allowed changing the coldkey while keeping the same hotkey, which caused issues because the new coldkey wouldn't own the original hotkey. Changes: - Add destination_hotkey parameter to transfer_stake dispatch (call_index 86) - Update do_transfer_stake to use separate origin_hotkey and destination_hotkey - Update StakeTransferred event to include both hotkeys - Update precompiles (transferStake now takes 6 params instead of 5) - Update chain-extensions for new signature - Update contract interface (ink! contracts) - Update solidity interface - Add test for transferring to different hotkey - Update all existing tests to use new parameter - Update documentation This is a breaking change for existing callers of transfer_stake. --- chain-extensions/src/lib.rs | 8 +- chain-extensions/src/tests.rs | 3 +- contract-tests/bittensor/lib.rs | 9 ++- docs/wasm-contracts.md | 2 +- pallets/subtensor/src/macros/dispatches.rs | 19 +++-- pallets/subtensor/src/macros/events.rs | 5 +- pallets/subtensor/src/staking/move_stake.rs | 27 ++++--- pallets/subtensor/src/tests/move_stake.rs | 87 +++++++++++++++++++++ pallets/subtensor/src/tests/subnet.rs | 2 + pallets/subtensor/src/tests/swap_coldkey.rs | 3 +- pallets/transaction-fee/src/lib.rs | 5 +- pallets/transaction-fee/src/tests/mod.rs | 3 +- precompiles/src/solidity/stakingV2.sol | 12 +-- precompiles/src/staking.rs | 11 ++- 14 files changed, 153 insertions(+), 43 deletions(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index eaaee70d27..1184e7d578 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -226,12 +226,13 @@ where } FunctionId::TransferStakeV1 => { let weight = Weight::from_parts(160_300_000, 0) - .saturating_add(T::DbWeight::get().reads(13_u64)) + .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)); env.charge_weight(weight)?; - let (destination_coldkey, hotkey, origin_netuid, destination_netuid, alpha_amount): ( + let (destination_coldkey, origin_hotkey, destination_hotkey, origin_netuid, destination_netuid, alpha_amount): ( + T::AccountId, T::AccountId, T::AccountId, NetUid, @@ -244,7 +245,8 @@ where let call_result = pallet_subtensor::Pallet::::transfer_stake( RawOrigin::Signed(env.caller()).into(), destination_coldkey, - hotkey, + origin_hotkey, + destination_hotkey, origin_netuid, destination_netuid, alpha_amount, diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index 094b0987e2..bf7a82c273 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -478,7 +478,7 @@ fn transfer_stake_success_moves_between_coldkeys() { let alpha_to_transfer: AlphaCurrency = (alpha_before.to_u64() / 3).into(); let expected_weight = Weight::from_parts(160_300_000, 0) - .saturating_add(::DbWeight::get().reads(13)) + .saturating_add(::DbWeight::get().reads(14)) .saturating_add(::DbWeight::get().writes(6)); let mut env = MockEnv::new( @@ -487,6 +487,7 @@ fn transfer_stake_success_moves_between_coldkeys() { ( destination_coldkey, hotkey, + hotkey, netuid, netuid, alpha_to_transfer, diff --git a/contract-tests/bittensor/lib.rs b/contract-tests/bittensor/lib.rs index 48e8d18aea..cfebe9726a 100755 --- a/contract-tests/bittensor/lib.rs +++ b/contract-tests/bittensor/lib.rs @@ -68,7 +68,8 @@ pub trait RuntimeReadWrite { #[ink(function = 6)] fn transfer_stake( destination_coldkey: ::AccountId, - hotkey: ::AccountId, + origin_hotkey: ::AccountId, + destination_hotkey: ::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, amount: AlphaCurrency, @@ -278,7 +279,8 @@ mod bittensor { pub fn transfer_stake( &self, destination_coldkey: [u8; 32], - hotkey: [u8; 32], + origin_hotkey: [u8; 32], + destination_hotkey: [u8; 32], origin_netuid: u16, destination_netuid: u16, amount: u64, @@ -287,7 +289,8 @@ mod bittensor { .extension() .transfer_stake( destination_coldkey.into(), - hotkey.into(), + origin_hotkey.into(), + destination_hotkey.into(), origin_netuid.into(), destination_netuid.into(), amount.into(), diff --git a/docs/wasm-contracts.md b/docs/wasm-contracts.md index ed6e9ecdd3..36c0f28842 100644 --- a/docs/wasm-contracts.md +++ b/docs/wasm-contracts.md @@ -34,7 +34,7 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 3 | `unstake_all` | Unstake all TAO from a hotkey | `(AccountId)` | Error code | | 4 | `unstake_all_alpha` | Unstake all Alpha from a hotkey | `(AccountId)` | Error code | | 5 | `move_stake` | Move stake between hotkeys | `(AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | -| 6 | `transfer_stake` | Transfer stake between coldkeys | `(AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | +| 6 | `transfer_stake` | Transfer stake between coldkeys and hotkeys | `(AccountId, AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | | 7 | `swap_stake` | Swap stake allocations between subnets | `(AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | | 8 | `add_stake_limit` | Delegate stake with a price limit | `(AccountId, NetUid, TaoCurrency, TaoCurrency, bool)` | Error code | | 9 | `remove_stake_limit` | Withdraw stake with a price limit | `(AccountId, NetUid, AlphaCurrency, TaoCurrency, bool)` | Error code | diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 61d5523285..c04ed8e818 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1636,13 +1636,14 @@ mod dispatches { ) } - /// Transfers a specified amount of stake from one coldkey to another, optionally across subnets, - /// while keeping the same hotkey. + /// Transfers a specified amount of stake from one coldkey to another, optionally across subnets + /// and hotkeys. /// /// # Arguments /// * `origin` - The origin of the transaction, which must be signed by the `origin_coldkey`. /// * `destination_coldkey` - The coldkey to which the stake is transferred. - /// * `hotkey` - The hotkey associated with the stake. + /// * `origin_hotkey` - The hotkey from which the stake is being transferred. + /// * `destination_hotkey` - The hotkey to which the stake is being transferred. /// * `origin_netuid` - The network/subnet ID to move stake from. /// * `destination_netuid` - The network/subnet ID to move stake to (for cross-subnet transfer). /// * `alpha_amount` - The amount of stake to transfer. @@ -1651,20 +1652,21 @@ mod dispatches { /// Returns an error if: /// * The origin is not signed by the correct coldkey. /// * Either subnet does not exist. - /// * The hotkey does not exist. - /// * There is insufficient stake on `(origin_coldkey, hotkey, origin_netuid)`. + /// * Either hotkey does not exist. + /// * There is insufficient stake on `(origin_coldkey, origin_hotkey, origin_netuid)`. /// * The transfer amount is below the minimum stake requirement. /// /// # Events /// May emit a `StakeTransferred` event on success. #[pallet::call_index(86)] #[pallet::weight((Weight::from_parts(160_300_000, 0) - .saturating_add(T::DbWeight::get().reads(13_u64)) + .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)), DispatchClass::Normal, Pays::Yes))] pub fn transfer_stake( origin: T::RuntimeOrigin, destination_coldkey: T::AccountId, - hotkey: T::AccountId, + origin_hotkey: T::AccountId, + destination_hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency, @@ -1672,7 +1674,8 @@ mod dispatches { Self::do_transfer_stake( origin, destination_coldkey, - hotkey, + origin_hotkey, + destination_hotkey, origin_netuid, destination_netuid, alpha_amount, diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a06e035d86..74f0b6df8b 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -274,13 +274,14 @@ mod events { /// - **error**: The dispatch error emitted by the failed item. BatchWeightItemFailed(sp_runtime::DispatchError), - /// Stake has been transferred from one coldkey to another on the same subnet. + /// Stake has been transferred from one coldkey to another, optionally across hotkeys and subnets. /// Parameters: - /// (origin_coldkey, destination_coldkey, hotkey, origin_netuid, destination_netuid, amount) + /// (origin_coldkey, destination_coldkey, origin_hotkey, destination_hotkey, origin_netuid, destination_netuid, amount) StakeTransferred( T::AccountId, T::AccountId, T::AccountId, + T::AccountId, NetUid, NetUid, TaoCurrency, diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index a1d9b46d5b..6ccb860a85 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -93,13 +93,14 @@ impl Pallet { Ok(()) } - /// Transfers stake from one coldkey to another, optionally moving from one subnet to another, - /// while keeping the same hotkey. + /// Transfers stake from one coldkey to another, optionally moving from one subnet to another + /// and from one hotkey to another. /// /// # Arguments /// * `origin` - The origin of the transaction, which must be signed by the `origin_coldkey`. /// * `destination_coldkey` - The account ID of the coldkey to which the stake is being transferred. - /// * `hotkey` - The account ID of the hotkey associated with this stake. + /// * `origin_hotkey` - The account ID of the hotkey from which the stake is being transferred. + /// * `destination_hotkey` - The account ID of the hotkey to which the stake is being transferred. /// * `origin_netuid` - The network ID (subnet) from which the stake is being transferred. /// * `destination_netuid` - The network ID (subnet) to which the stake is being transferred. /// * `alpha_amount` - The amount of stake to transfer. @@ -111,8 +112,8 @@ impl Pallet { /// This function will return an error if: /// * The transaction is not signed by the `origin_coldkey`. /// * The subnet (`origin_netuid` or `destination_netuid`) does not exist. - /// * The `hotkey` does not exist. - /// * The `(origin_coldkey, hotkey, origin_netuid)` does not have enough stake for `alpha_amount`. + /// * Either `origin_hotkey` or `destination_hotkey` does not exist. + /// * The `(origin_coldkey, origin_hotkey, origin_netuid)` does not have enough stake for `alpha_amount`. /// * The amount to be transferred is below the minimum stake requirement. /// * There is a failure in staking or unstaking logic. /// @@ -121,7 +122,8 @@ impl Pallet { pub fn do_transfer_stake( origin: T::RuntimeOrigin, destination_coldkey: T::AccountId, - hotkey: T::AccountId, + origin_hotkey: T::AccountId, + destination_hotkey: T::AccountId, origin_netuid: NetUid, destination_netuid: NetUid, alpha_amount: AlphaCurrency, @@ -133,8 +135,8 @@ impl Pallet { let tao_moved = Self::transition_stake_internal( &coldkey, &destination_coldkey, - &hotkey, - &hotkey, + &origin_hotkey, + &destination_hotkey, origin_netuid, destination_netuid, alpha_amount, @@ -144,20 +146,21 @@ impl Pallet { false, )?; - // 9. Emit an event for logging/monitoring. + // Emit an event for logging/monitoring. log::debug!( - "StakeTransferred(origin_coldkey: {coldkey:?}, destination_coldkey: {destination_coldkey:?}, hotkey: {hotkey:?}, origin_netuid: {origin_netuid:?}, destination_netuid: {destination_netuid:?}, amount: {tao_moved:?})" + "StakeTransferred(origin_coldkey: {coldkey:?}, destination_coldkey: {destination_coldkey:?}, origin_hotkey: {origin_hotkey:?}, destination_hotkey: {destination_hotkey:?}, origin_netuid: {origin_netuid:?}, destination_netuid: {destination_netuid:?}, amount: {tao_moved:?})" ); Self::deposit_event(Event::StakeTransferred( coldkey, destination_coldkey, - hotkey, + origin_hotkey, + destination_hotkey, origin_netuid, destination_netuid, tao_moved, )); - // 10. Return success. + // Return success. Ok(()) } diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index dfd9927da4..aad6829d0c 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -925,6 +925,7 @@ fn test_do_transfer_success() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, netuid, netuid, alpha @@ -965,6 +966,7 @@ fn test_do_transfer_nonexistent_subnet() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, nonexistent_netuid, nonexistent_netuid, stake_amount.into() @@ -990,6 +992,7 @@ fn test_do_transfer_nonexistent_hotkey() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, nonexistent_hotkey, + nonexistent_hotkey, netuid, netuid, 100.into() @@ -1030,6 +1033,7 @@ fn test_do_transfer_insufficient_stake() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, netuid, netuid, alpha.into() @@ -1069,6 +1073,7 @@ fn test_do_transfer_wrong_origin() { RuntimeOrigin::signed(wrong_coldkey), destination_coldkey, hotkey, + hotkey, netuid, netuid, stake_amount.into() @@ -1107,6 +1112,7 @@ fn test_do_transfer_minimum_stake_check() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, netuid, netuid, 1.into() @@ -1164,6 +1170,7 @@ fn test_do_transfer_different_subnets() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, origin_netuid, destination_netuid, alpha @@ -1192,6 +1199,84 @@ fn test_do_transfer_different_subnets() { }); } +#[test] +fn test_do_transfer_to_different_hotkey() { + new_test_ext(1).execute_with(|| { + // 1. Setup: Create a subnet and two hotkeys + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let origin_coldkey = U256::from(1); + let destination_coldkey = U256::from(2); + let origin_hotkey = U256::from(3); + let destination_hotkey = U256::from(4); + let stake_amount = DefaultMinStake::::get().to_u64() * 10; + + // 2. Create accounts for both hotkeys + SubtensorModule::create_account_if_non_existent(&origin_coldkey, &origin_hotkey); + SubtensorModule::create_account_if_non_existent(&destination_coldkey, &destination_hotkey); + + // 3. Stake into the origin hotkey + SubtensorModule::stake_into_subnet( + &origin_hotkey, + &origin_coldkey, + netuid, + stake_amount.into(), + ::SwapInterface::max_price(), + false, + false, + ) + .unwrap(); + + let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &origin_coldkey, + netuid, + ); + + // 4. Transfer stake to a DIFFERENT coldkey AND DIFFERENT hotkey + assert_ok!(SubtensorModule::do_transfer_stake( + RuntimeOrigin::signed(origin_coldkey), + destination_coldkey, + origin_hotkey, + destination_hotkey, + netuid, + netuid, + alpha + )); + + // 5. Verify origin coldkey + origin hotkey has no stake + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &origin_coldkey, + netuid + ), + AlphaCurrency::ZERO + ); + + // 6. Verify destination coldkey + destination hotkey has the stake + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &destination_coldkey, + netuid + ) > AlphaCurrency::ZERO + ); + + // 7. Verify origin coldkey + destination hotkey has NO stake (stake went to destination coldkey) + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &origin_coldkey, + netuid + ), + AlphaCurrency::ZERO + ); + }); +} + #[test] fn test_do_swap_success() { new_test_ext(1).execute_with(|| { @@ -1819,6 +1904,7 @@ fn test_transfer_stake_rate_limited() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, netuid, netuid, alpha @@ -1863,6 +1949,7 @@ fn test_transfer_stake_doesnt_limit_destination_coldkey() { RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, + hotkey, netuid, netuid2, alpha diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index d2a73a919d..6d9e246528 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -538,6 +538,7 @@ fn test_subtoken_enable_reject_trading_before_enable() { RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, hotkey_account_2_id, + hotkey_account_2_id, netuid, netuid2, amount.into(), @@ -659,6 +660,7 @@ fn test_subtoken_enable_trading_ok_with_enable() { RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, hotkey_account_2_id, + hotkey_account_2_id, netuid, netuid2, unstake_amount, diff --git a/pallets/subtensor/src/tests/swap_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index 9d3bdbfc62..a55f37fb66 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -2335,7 +2335,8 @@ fn test_coldkey_in_swap_schedule_prevents_funds_usage() { // Transfer stake let call = RuntimeCall::SubtensorModule(SubtensorCall::transfer_stake { destination_coldkey: new_coldkey, - hotkey, + origin_hotkey: hotkey, + destination_hotkey: hotkey, origin_netuid: netuid, destination_netuid: netuid, alpha_amount: stake.into(), diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index fc2a16a409..3c667a693d 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -264,10 +264,11 @@ impl SubtensorTxFeeHandler { }) => alpha_vec.push((origin_hotkey.clone(), *origin_netuid)), Some(SubtensorCall::transfer_stake { destination_coldkey: _, - hotkey, + origin_hotkey, + destination_hotkey: _, origin_netuid, .. - }) => alpha_vec.push((hotkey.clone(), *origin_netuid)), + }) => alpha_vec.push((origin_hotkey.clone(), *origin_netuid)), Some(SubtensorCall::swap_stake { hotkey, origin_netuid, diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index b6697e87f0..2a5fa6e8ed 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -901,7 +901,8 @@ fn test_transfer_stake_fees_alpha() { ); let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { destination_coldkey, - hotkey: sn.hotkeys[0], + origin_hotkey: sn.hotkeys[0], + destination_hotkey: sn.hotkeys[0], origin_netuid: sn.subnets[0].netuid, destination_netuid: sn.subnets[1].netuid, alpha_amount: unstake_amount, diff --git a/precompiles/src/solidity/stakingV2.sol b/precompiles/src/solidity/stakingV2.sol index fefca82fd9..cee0554e33 100644 --- a/precompiles/src/solidity/stakingV2.sol +++ b/precompiles/src/solidity/stakingV2.sol @@ -80,16 +80,17 @@ interface IStaking { /** * @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey - * `destination_coldkey`. + * `destination_coldkey` and optionally a different hotkey `destination_hotkey`. * - * This function allows external accounts and contracts to transfer staked TAO to another coldkey, + * This function allows external accounts and contracts to transfer staked TAO to another coldkey and hotkey, * which effectively calls `transfer_stake` on the subtensor pallet with specified destination - * coldkey as a parameter being the hashed address mapping of H160 sender address to Substrate ss58 + * coldkey and hotkey as parameters being the hashed address mapping of H160 sender address to Substrate ss58 * address as implemented in Frontier HashedAddressMapping: * https://github.com/polkadot-evm/frontier/blob/2e219e17a526125da003e64ef22ec037917083fa/frame/evm/src/lib.rs#L739 * * @param destination_coldkey The destination coldkey public key (32 bytes). - * @param hotkey The hotkey public key (32 bytes). + * @param origin_hotkey The origin hotkey public key (32 bytes). + * @param destination_hotkey The destination hotkey public key (32 bytes). * @param origin_netuid The subnet to move stake from (uint256). * @param destination_netuid The subnet to move stake to (uint256). * @param amount The amount to move in rao. @@ -100,7 +101,8 @@ interface IStaking { */ function transferStake( bytes32 destination_coldkey, - bytes32 hotkey, + bytes32 origin_hotkey, + bytes32 destination_hotkey, uint256 origin_netuid, uint256 destination_netuid, uint256 amount diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 35daaf4f47..c3899a3a32 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -190,25 +190,28 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } - #[precompile::public("transferStake(bytes32,bytes32,uint256,uint256,uint256)")] + #[precompile::public("transferStake(bytes32,bytes32,bytes32,uint256,uint256,uint256)")] #[precompile::payable] fn transfer_stake( handle: &mut impl PrecompileHandle, destination_coldkey: H256, - hotkey: H256, + origin_hotkey: H256, + destination_hotkey: H256, origin_netuid: U256, destination_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let destination_coldkey = R::AccountId::from(destination_coldkey.0); - let hotkey = R::AccountId::from(hotkey.0); + let origin_hotkey = R::AccountId::from(origin_hotkey.0); + let destination_hotkey = R::AccountId::from(destination_hotkey.0); let origin_netuid = try_u16_from_u256(origin_netuid)?; let destination_netuid = try_u16_from_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::transfer_stake { destination_coldkey, - hotkey, + origin_hotkey, + destination_hotkey, origin_netuid: origin_netuid.into(), destination_netuid: destination_netuid.into(), alpha_amount: alpha_amount.into(), From 18714cf3a11efb8d6c838b8b780215ea882b892d Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Wed, 17 Dec 2025 14:17:02 +0200 Subject: [PATCH 2/2] feat: add backward-compatible EVM interface for transfer_stake - Keep transferStake with 5 params (same hotkey) for backward compatibility - Add transferStakeV2 with 6 params (different origin/destination hotkeys) - Update chain-extensions with TransferStakeV1 (5 params) and TransferStakeV2 (6 params) - Update contract-tests and docs accordingly --- chain-extensions/src/lib.rs | 46 +++++++++++++++++++++++++- chain-extensions/src/tests.rs | 1 - chain-extensions/src/types.rs | 1 + contract-tests/bittensor/lib.rs | 37 ++++++++++++++++++++- docs/wasm-contracts.md | 3 +- precompiles/src/solidity/stakingV2.sol | 30 ++++++++++++++++- precompiles/src/staking.rs | 32 +++++++++++++++++- 7 files changed, 144 insertions(+), 6 deletions(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 1184e7d578..2b61feb8a7 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -224,6 +224,7 @@ where } } } + // Backward-compatible TransferStakeV1 with 5 parameters (same hotkey for origin and destination) FunctionId::TransferStakeV1 => { let weight = Weight::from_parts(160_300_000, 0) .saturating_add(T::DbWeight::get().reads(14_u64)) @@ -231,7 +232,50 @@ where env.charge_weight(weight)?; - let (destination_coldkey, origin_hotkey, destination_hotkey, origin_netuid, destination_netuid, alpha_amount): ( + let (destination_coldkey, hotkey, origin_netuid, destination_netuid, alpha_amount): ( + T::AccountId, + T::AccountId, + NetUid, + NetUid, + AlphaCurrency, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::transfer_stake( + RawOrigin::Signed(env.caller()).into(), + destination_coldkey, + hotkey.clone(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + // New TransferStakeV2 with 6 parameters (allows different origin and destination hotkeys) + FunctionId::TransferStakeV2 => { + let weight = Weight::from_parts(160_300_000, 0) + .saturating_add(T::DbWeight::get().reads(14_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)); + + env.charge_weight(weight)?; + + let ( + destination_coldkey, + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ): ( T::AccountId, T::AccountId, T::AccountId, diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index bf7a82c273..84f59d0635 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -487,7 +487,6 @@ fn transfer_stake_success_moves_between_coldkeys() { ( destination_coldkey, hotkey, - hotkey, netuid, netuid, alpha_to_transfer, diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index ee6298ad5b..d6d1b4d166 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -21,6 +21,7 @@ pub enum FunctionId { AddProxyV1 = 13, RemoveProxyV1 = 14, GetAlphaPriceV1 = 15, + TransferStakeV2 = 16, } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] diff --git a/contract-tests/bittensor/lib.rs b/contract-tests/bittensor/lib.rs index cfebe9726a..a62c6985a5 100755 --- a/contract-tests/bittensor/lib.rs +++ b/contract-tests/bittensor/lib.rs @@ -23,6 +23,7 @@ pub enum FunctionId { AddProxyV1 = 13, RemoveProxyV1 = 14, GetAlphaPriceV1 = 15, + TransferStakeV2 = 16, } #[ink::chain_extension(extension = 0x1000)] @@ -65,8 +66,19 @@ pub trait RuntimeReadWrite { amount: AlphaCurrency, ); + // Backward-compatible transfer_stake with 5 parameters (same hotkey) #[ink(function = 6)] fn transfer_stake( + destination_coldkey: ::AccountId, + hotkey: ::AccountId, + origin_netuid: NetUid, + destination_netuid: NetUid, + amount: AlphaCurrency, + ); + + // New transfer_stake_v2 with 6 parameters (different hotkeys) + #[ink(function = 16)] + fn transfer_stake_v2( destination_coldkey: ::AccountId, origin_hotkey: ::AccountId, destination_hotkey: ::AccountId, @@ -275,8 +287,31 @@ mod bittensor { .map_err(|_e| ReadWriteErrorCode::WriteFailed) } + // Backward-compatible transfer_stake with 5 parameters (same hotkey) #[ink(message)] pub fn transfer_stake( + &self, + destination_coldkey: [u8; 32], + hotkey: [u8; 32], + origin_netuid: u16, + destination_netuid: u16, + amount: u64, + ) -> Result<(), ReadWriteErrorCode> { + self.env() + .extension() + .transfer_stake( + destination_coldkey.into(), + hotkey.into(), + origin_netuid.into(), + destination_netuid.into(), + amount.into(), + ) + .map_err(|_e| ReadWriteErrorCode::WriteFailed) + } + + // New transfer_stake_v2 with 6 parameters (different hotkeys) + #[ink(message)] + pub fn transfer_stake_v2( &self, destination_coldkey: [u8; 32], origin_hotkey: [u8; 32], @@ -287,7 +322,7 @@ mod bittensor { ) -> Result<(), ReadWriteErrorCode> { self.env() .extension() - .transfer_stake( + .transfer_stake_v2( destination_coldkey.into(), origin_hotkey.into(), destination_hotkey.into(), diff --git a/docs/wasm-contracts.md b/docs/wasm-contracts.md index 36c0f28842..2a0b159fae 100644 --- a/docs/wasm-contracts.md +++ b/docs/wasm-contracts.md @@ -34,7 +34,8 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 3 | `unstake_all` | Unstake all TAO from a hotkey | `(AccountId)` | Error code | | 4 | `unstake_all_alpha` | Unstake all Alpha from a hotkey | `(AccountId)` | Error code | | 5 | `move_stake` | Move stake between hotkeys | `(AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | -| 6 | `transfer_stake` | Transfer stake between coldkeys and hotkeys | `(AccountId, AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | +| 6 | `transfer_stake` | Transfer stake between coldkeys (same hotkey) | `(AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | +| 16 | `transfer_stake_v2` | Transfer stake between coldkeys and hotkeys | `(AccountId, AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | | 7 | `swap_stake` | Swap stake allocations between subnets | `(AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | | 8 | `add_stake_limit` | Delegate stake with a price limit | `(AccountId, NetUid, TaoCurrency, TaoCurrency, bool)` | Error code | | 9 | `remove_stake_limit` | Withdraw stake with a price limit | `(AccountId, NetUid, AlphaCurrency, TaoCurrency, bool)` | Error code | diff --git a/precompiles/src/solidity/stakingV2.sol b/precompiles/src/solidity/stakingV2.sol index cee0554e33..0bdcd80b38 100644 --- a/precompiles/src/solidity/stakingV2.sol +++ b/precompiles/src/solidity/stakingV2.sol @@ -78,6 +78,34 @@ interface IStaking { uint256 amount ) external payable; + /** + * @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey + * `destination_coldkey` while keeping the same hotkey. + * + * This function allows external accounts and contracts to transfer staked TAO to another coldkey, + * which effectively calls `transfer_stake` on the subtensor pallet with specified destination + * coldkey as a parameter being the hashed address mapping of H160 sender address to Substrate ss58 + * address as implemented in Frontier HashedAddressMapping: + * https://github.com/polkadot-evm/frontier/blob/2e219e17a526125da003e64ef22ec037917083fa/frame/evm/src/lib.rs#L739 + * + * @param destination_coldkey The destination coldkey public key (32 bytes). + * @param hotkey The hotkey public key (32 bytes). + * @param origin_netuid The subnet to move stake from (uint256). + * @param destination_netuid The subnet to move stake to (uint256). + * @param amount The amount to move in rao. + * + * Requirements: + * - `hotkey` must be a valid hotkey registered on the network, ensuring + * that the stake is correctly attributed. + */ + function transferStake( + bytes32 destination_coldkey, + bytes32 hotkey, + uint256 origin_netuid, + uint256 destination_netuid, + uint256 amount + ) external payable; + /** * @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey * `destination_coldkey` and optionally a different hotkey `destination_hotkey`. @@ -99,7 +127,7 @@ interface IStaking { * - `origin_hotkey` and `destination_hotkey` must be valid hotkeys registered on the network, ensuring * that the stake is correctly attributed. */ - function transferStake( + function transferStakeV2( bytes32 destination_coldkey, bytes32 origin_hotkey, bytes32 destination_hotkey, diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index c3899a3a32..90075f14d1 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -190,9 +190,39 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } - #[precompile::public("transferStake(bytes32,bytes32,bytes32,uint256,uint256,uint256)")] + /// Backward-compatible transferStake with 5 parameters (same hotkey for origin and destination) + #[precompile::public("transferStake(bytes32,bytes32,uint256,uint256,uint256)")] #[precompile::payable] fn transfer_stake( + handle: &mut impl PrecompileHandle, + destination_coldkey: H256, + hotkey: H256, + origin_netuid: U256, + destination_netuid: U256, + amount_alpha: U256, + ) -> EvmResult<()> { + let account_id = handle.caller_account_id::(); + let destination_coldkey = R::AccountId::from(destination_coldkey.0); + let hotkey = R::AccountId::from(hotkey.0); + let origin_netuid = try_u16_from_u256(origin_netuid)?; + let destination_netuid = try_u16_from_u256(destination_netuid)?; + let alpha_amount: u64 = amount_alpha.unique_saturated_into(); + let call = pallet_subtensor::Call::::transfer_stake { + destination_coldkey, + origin_hotkey: hotkey.clone(), + destination_hotkey: hotkey, + origin_netuid: origin_netuid.into(), + destination_netuid: destination_netuid.into(), + alpha_amount: alpha_amount.into(), + }; + + handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) + } + + /// New transferStakeV2 with 6 parameters (allows different origin and destination hotkeys) + #[precompile::public("transferStakeV2(bytes32,bytes32,bytes32,uint256,uint256,uint256)")] + #[precompile::payable] + fn transfer_stake_v2( handle: &mut impl PrecompileHandle, destination_coldkey: H256, origin_hotkey: H256,