diff --git a/contract-tests/src/contracts/staking.ts b/contract-tests/src/contracts/staking.ts index 32957342b4..5202909849 100644 --- a/contract-tests/src/contracts/staking.ts +++ b/contract-tests/src/contracts/staking.ts @@ -454,5 +454,141 @@ export const IStakingV2ABI = [ "outputs": [], "stateMutability": "payable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "absoluteAmount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "sourceColdkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "increaseAmount", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [], + "stateMutability": "", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decreaseAmount", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [], + "stateMutability": "", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "source_coldkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "destination_coldkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "origin_netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "destination_netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferStakeFrom", + "outputs": [], + "stateMutability": "", + "type": "function" } -]; \ No newline at end of file +]; diff --git a/contract-tests/test/staking.precompile.approval.test.ts b/contract-tests/test/staking.precompile.approval.test.ts new file mode 100644 index 0000000000..b6dfc9e6b5 --- /dev/null +++ b/contract-tests/test/staking.precompile.approval.test.ts @@ -0,0 +1,241 @@ +import * as assert from "assert"; +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" +import { raoToEth, tao } from "../src/balance-math" +import { ethers } from "ethers" +import { generateRandomEthersWallet, getPublicClient } from "../src/utils" +import { convertH160ToPublicKey } from "../src/address-utils" +import { + forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister, + sendProxyCall, + startCall, +} from "../src/subtensor" +import { ETH_LOCAL_URL } from "../src/config"; +import { ISTAKING_ADDRESS, ISTAKING_V2_ADDRESS, IStakingABI, IStakingV2ABI } from "../src/contracts/staking" +import { PublicClient } from "viem"; + +describe("Test approval in staking precompile", () => { + // init eth part + const wallet1 = generateRandomEthersWallet(); + const wallet2 = generateRandomEthersWallet(); + let publicClient: PublicClient; + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const proxy = getRandomSubstrateKeypair(); + + let api: TypedApi + let stakeNetuid: number; + + let expectedAllowance = BigInt(0); + + // sudo account alice as signer + let alice: PolkadotSigner; + before(async () => { + publicClient = await getPublicClient(ETH_LOCAL_URL) + // init variables got from await and async + api = await getDevnetApi() + + // await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(proxy.publicKey)) + await forceSetBalanceToEthAddress(api, wallet1.address) + await forceSetBalanceToEthAddress(api, wallet2.address) + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + await startCall(api, netuid, coldkey) + + console.log("test the case on subnet ", netuid) + + await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey) + await burnedRegister(api, netuid, convertH160ToSS58(wallet2.address), coldkey) + + // add stake as wallet1 + { + stakeNetuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + // the unit in V2 is RAO, not ETH + let stakeBalance = tao(20) + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), stakeNetuid) + await tx.wait() + + const stakeFromContract = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), stakeNetuid) + ); + + assert.ok(stakeFromContract > stakeBefore) + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid) + assert.ok(stakeAfter > stakeBefore) + } + }) + + it("Can't transfer from account without approval", async () => { + try { + // wallet2 tries to transfer from wallet1 + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); + const tx = await contract.transferStakeFrom( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // distination + hotkey.publicKey, + stakeNetuid, + stakeNetuid, + 1 + ) + await tx.wait(); + + assert.fail("should have reverted due to missing allowance"); + } catch (e) { + assert.equal(e.reason, "trying to spend more than allowed", "wrong revert message"); + } + }) + + it("Can approve some amount", async () => { + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + + { + let allowance = BigInt( + await contract.allowance( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + ) + ); + assert.equal(allowance, expectedAllowance, "default allowance should be 0"); + } + + { + const tx = await contract.approve( + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + tao(10) + ) + await tx.wait(); + + expectedAllowance += BigInt(tao(10)); + + let allowance = BigInt( + await contract.allowance( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + ) + ); + assert.equal(allowance, expectedAllowance, "should have set allowance"); + } + }) + + it("Can now use transfer from", async () => { + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); + + // wallet2 transfer from wallet1 + const tx = await contract.transferStakeFrom( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // distination + hotkey.publicKey, + stakeNetuid, + stakeNetuid, + tao(5) + ) + await tx.wait(); + + expectedAllowance -= BigInt(tao(5)); + + { + let allowance = BigInt( + await contract.allowance( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + ) + ); + assert.equal(allowance, expectedAllowance, "allowance should now be 500"); + } + }) + + it("Can't use transfer from with amount too high", async () => { + try { + // wallet2 tries to transfer from wallet1 + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); + const tx = await contract.transferStakeFrom( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // distination + hotkey.publicKey, + stakeNetuid, + stakeNetuid, + expectedAllowance + BigInt(1) + ) + await tx.wait(); + + assert.fail("should have reverted due to missing allowance"); + } catch (e) { + assert.equal(e.reason, "trying to spend more than allowed", "wrong revert message"); + } + }) + + it("Approval functions works as expected", async () => { + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + + { + const tx = await contract.increaseAllowance( + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + tao(10) + ) + await tx.wait(); + + expectedAllowance += BigInt(tao(10)); + + let allowance = BigInt( + await contract.allowance( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + ) + ); + assert.equal(allowance, expectedAllowance, "allowance have been increased correctly"); + } + + { + const tx = await contract.decreaseAllowance( + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + tao(2) + ) + await tx.wait(); + + expectedAllowance -= BigInt(tao(2)); + + let allowance = BigInt( + await contract.allowance( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + ) + ); + assert.equal(allowance, expectedAllowance, "allowance have been decreased correctly"); + } + + { + const tx = await contract.approve( + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + 0 + ) + await tx.wait(); + + expectedAllowance = BigInt(0); + + let allowance = BigInt( + await contract.allowance( + convertH160ToPublicKey(wallet1.address), // source + convertH160ToPublicKey(wallet2.address), // destination + stakeNetuid, + ) + ); + assert.equal(allowance, expectedAllowance, "allowance have been overwritten correctly"); + } + }) +}) diff --git a/precompiles/src/solidity/stakingV2.abi b/precompiles/src/solidity/stakingV2.abi index 40a5acc1d9..14140912b4 100644 --- a/precompiles/src/solidity/stakingV2.abi +++ b/precompiles/src/solidity/stakingV2.abi @@ -394,5 +394,141 @@ "outputs": [], "stateMutability": "payable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "absoluteAmount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "sourceColdkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "increaseAmount", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [], + "stateMutability": "", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "spenderColdkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decreaseAmount", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [], + "stateMutability": "", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "source_coldkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "destination_coldkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "origin_netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "destination_netuid", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferStakeFrom", + "outputs": [], + "stateMutability": "", + "type": "function" } ] diff --git a/precompiles/src/solidity/stakingV2.sol b/precompiles/src/solidity/stakingV2.sol index fefca82fd9..7b14f6a041 100644 --- a/precompiles/src/solidity/stakingV2.sol +++ b/precompiles/src/solidity/stakingV2.sol @@ -310,4 +310,106 @@ interface IStaking { uint256 amount, uint256 netuid ) external payable; + + /** + * @dev Set how much the caller approves the spender to use the provided amount of subnet tokens + * on its behalf in a later call. + * + * This is similar to ERC20 approve, and then allows smart contract to transfer with permission from + * other accounts during their execution. They can then act as escrows while knowing from whom + * the funds comes from, which is not possible if the spender called `transfer` towards the contract + * (no callback). + * + * @param spenderColdkey Coldkey of the address allowed to spend funds from the caller. + * @param netuid The approved subnet token. + * @param absoluteAmount New approval amount, will overwrite previous value. + */ + function approve( + bytes32 spenderColdkey, + uint256 netuid, + uint256 absoluteAmount + ) external; + + /** + * @dev Get how much the source allows the spender to use their subnet tokens + * + * @param spenderColdkey Coldkey of the source making the allowance. + * @param spenderColdkey Coldkey of the address allowed to spend funds from the source. + * @param netuid The approved subnet token. + */ + function allowance( + bytes32 sourceColdkey, + bytes32 spenderColdkey, + uint256 netuid, + ) external view returns (uint256); + + /** + * @dev Increase how much the caller approves the spender to use the provided amount of subnet tokens + * on its behalf in a later call. + * + * This is similar to ERC20 increaseAllowance, and then allows smart contract to transfer with permission from + * other accounts during their execution. They can then act as escrows while knowing from whom + * the funds comes from, which is not possible if the spender called `transfer` towards the contract + * (no callback). + * + * @param spenderColdkey Coldkey of the address allowed to spend funds from the caller. + * @param netuid The approved subnet token. + * @param increaseAmount How much the approval amount should be increased. + */ + function increaseAllowance( + bytes32 spenderColdkey, + uint256 netuid, + uint256 increaseAmount + ) external; + + /** + * @dev Decrease how much the caller approves the spender to use the provided amount of subnet tokens + * on its behalf in a later call. + * + * This is similar to ERC20 decreaseAllowance, and then allows smart contract to transfer with permission from + * other accounts during their execution. They can then act as escrows while knowing from whom + * the funds comes from, which is not possible if the spender called `transfer` towards the contract + * (no callback). + * + * @param spenderColdkey Coldkey of the address allowed to spend funds from the caller. + * @param netuid The approved subnet token. + * @param increaseAmount How much the approval amount should be decrease. + */ + function decreaseAllowance( + bytes32 spenderColdkey, + uint256 netuid, + uint256 decreaseAmount + ) external; + + /** + * @dev Transfer a subtensor stake `amount` associated with the `source_coldkey` to a different coldkey + * `destination_coldkey`. The `source_coldkey` must have approved beforehand the transaction signer + * (spender) to spend at least the `amount` (allowance). The allowance towards that spender will be + * decreased by this amount. + * + * 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 source_coldkey The source coldkey public key (32 bytes). + * @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: + * - `origin_hotkey` and `destination_hotkey` must be valid hotkeys registered on the network, ensuring + * that the stake is correctly attributed. + */ + function transferStakeFrom( + bytes32 source_coldkey + bytes32 destination_coldkey, + bytes32 hotkey, + uint256 origin_netuid, + uint256 destination_netuid, + uint256 amount + ) external; } diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index c276c32e60..b3b8cad5ab 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -27,8 +27,10 @@ use alloc::vec::Vec; use core::marker::PhantomData; +use frame_support::Blake2_128Concat; use frame_support::dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}; -use frame_support::traits::IsSubType; +use frame_support::pallet_prelude::{StorageDoubleMap, ValueQuery}; +use frame_support::traits::{IsSubType, StorageInstance}; use frame_system::RawOrigin; use pallet_evm::{ AddressMapping, BalanceConverter, EvmBalance, ExitError, PrecompileFailure, PrecompileHandle, @@ -36,6 +38,7 @@ use pallet_evm::{ }; use pallet_subtensor_proxy as pallet_proxy; use precompile_utils::EvmResult; +use precompile_utils::prelude::{RuntimeHelper, revert}; use sp_core::{H256, U256}; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup, UniqueSaturatedInto}; use sp_std::vec; @@ -43,6 +46,29 @@ use subtensor_runtime_common::{Currency, NetUid, ProxyType}; use crate::{PrecompileExt, PrecompileHandleExt}; +/// Prefix for the Allowances map in Substrate storage. +pub struct AllowancesPrefix; +impl StorageInstance for AllowancesPrefix { + const STORAGE_PREFIX: &'static str = "Allowances"; + + fn pallet_prefix() -> &'static str { + "EvmPrecompileStaking" + } +} + +pub type AllowancesStorage = StorageDoubleMap< + AllowancesPrefix, + // For each approver + Blake2_128Concat, + ::AccountId, + // For each pair of (spender, netuid) + Blake2_128Concat, + (::AccountId, u16), + // Allowed amount + U256, + ValueQuery, +>; + // Old StakingPrecompile had ETH-precision in values, which was not alligned with Substrate API. So // it's kinda deprecated, but exists for backward compatibility. Eventually, we should remove it // to stop supporting both precompiles. @@ -447,6 +473,177 @@ where Ok(stake.to_u64().into()) } + + #[precompile::public("approve(bytes32,uint256,uint256)")] + fn approve( + handle: &mut impl PrecompileHandle, + spender_coldkey: H256, + origin_netuid: U256, + amount_alpha: U256, + ) -> EvmResult<()> { + // AllowancesStorage write + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + + let approver = handle.caller_account_id::(); + let spender = R::AccountId::from(spender_coldkey.0); + let netuid = try_u16_from_u256(origin_netuid)?; + + if amount_alpha.is_zero() { + AllowancesStorage::::remove(approver, (spender, netuid)); + } else { + AllowancesStorage::::insert(approver, (spender, netuid), amount_alpha); + } + + Ok(()) + } + + #[precompile::public("allowance(bytes32,bytes32,uint256)")] + #[precompile::view] + fn allowance( + handle: &mut impl PrecompileHandle, + source_coldkey: H256, + spender_coldkey: H256, + origin_netuid: U256, + ) -> EvmResult { + // AllowancesStorage read + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + + let source = R::AccountId::from(source_coldkey.0); + let spender = R::AccountId::from(spender_coldkey.0); + let netuid = try_u16_from_u256(origin_netuid)?; + + Ok(AllowancesStorage::::get(source, (spender, netuid))) + } + + #[precompile::public("increaseAllowance(bytes32,uint256,uint256)")] + fn increase_allowance( + handle: &mut impl PrecompileHandle, + spender_coldkey: H256, + origin_netuid: U256, + amount_alpha_increase: U256, + ) -> EvmResult<()> { + if amount_alpha_increase.is_zero() { + return Ok(()); + } + + // AllowancesStorage read + write + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + + let approver = handle.caller_account_id::(); + let spender = R::AccountId::from(spender_coldkey.0); + let netuid = try_u16_from_u256(origin_netuid)?; + + let approval_key = (spender, netuid); + + let current_amount = AllowancesStorage::::get(&approver, &approval_key); + let new_amount = current_amount.saturating_add(amount_alpha_increase); + + AllowancesStorage::::insert(approver, &approval_key, new_amount); + + Ok(()) + } + + #[precompile::public("decreaseAllowance(bytes32,uint256,uint256)")] + fn decrease_allowance( + handle: &mut impl PrecompileHandle, + spender_coldkey: H256, + origin_netuid: U256, + amount_alpha_decrease: U256, + ) -> EvmResult<()> { + if amount_alpha_decrease.is_zero() { + return Ok(()); + } + + // AllowancesStorage read + write + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + + let approver = handle.caller_account_id::(); + let spender = R::AccountId::from(spender_coldkey.0); + let netuid = try_u16_from_u256(origin_netuid)?; + + let approval_key = (spender, netuid); + + let current_amount = AllowancesStorage::::get(&approver, &approval_key); + let new_amount = current_amount.saturating_sub(amount_alpha_decrease); + + if new_amount.is_zero() { + AllowancesStorage::::remove(approver, &approval_key); + } else { + AllowancesStorage::::insert(approver, &approval_key, new_amount); + } + + Ok(()) + } + + fn try_consume_allowance( + handle: &mut impl PrecompileHandle, + approver: R::AccountId, + spender: R::AccountId, + netuid: u16, + amount: U256, + ) -> EvmResult<()> { + if amount.is_zero() { + return Ok(()); + } + + // AllowancesStorage read + write + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + + let approval_key = (spender, netuid); + + let current_amount = AllowancesStorage::::get(&approver, &approval_key); + let Some(new_amount) = current_amount.checked_sub(amount) else { + return Err(revert("trying to spend more than allowed")); + }; + + if new_amount.is_zero() { + AllowancesStorage::::remove(approver, &approval_key); + } else { + AllowancesStorage::::insert(approver, &approval_key, new_amount); + } + + Ok(()) + } + + #[precompile::public("transferStakeFrom(bytes32,bytes32,bytes32,uint256,uint256,uint256)")] + fn transfer_stake_from( + handle: &mut impl PrecompileHandle, + source_coldkey: H256, + destination_coldkey: H256, + hotkey: H256, + origin_netuid: U256, + destination_netuid: U256, + amount_alpha: U256, + ) -> EvmResult<()> { + let spender_id = handle.caller_account_id::(); + let source_id = R::AccountId::from(source_coldkey.0); + 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(); + + Self::try_consume_allowance( + handle, + source_id.clone(), + spender_id, + origin_netuid, + amount_alpha, + )?; + + let call = pallet_subtensor::Call::::transfer_stake { + destination_coldkey, + hotkey, + origin_netuid: origin_netuid.into(), + destination_netuid: destination_netuid.into(), + alpha_amount: alpha_amount.into(), + }; + + handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(source_id)) + } } // Deprecated, exists for backward compatibility.