From 2eb3c8cfbbb2dea014a748aa23b42959b8e2c632 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 17 Apr 2025 18:03:26 -0400 Subject: [PATCH 1/2] impl --- evm-tests/src/contracts/staking.ts | 68 +++++++++++++++++++++++- precompiles/src/solidity/stakingV2.abi | 68 +++++++++++++++++++++++- precompiles/src/solidity/stakingV2.sol | 71 +++++++++++++++++++++++--- precompiles/src/staking.rs | 50 ++++++++++++++++++ 4 files changed, 247 insertions(+), 10 deletions(-) diff --git a/evm-tests/src/contracts/staking.ts b/evm-tests/src/contracts/staking.ts index af4422ca96..0ba37c5a94 100644 --- a/evm-tests/src/contracts/staking.ts +++ b/evm-tests/src/contracts/staking.ts @@ -287,5 +287,71 @@ export const IStakingV2ABI = [ "outputs": [], "stateMutability": "nonpayable", "type": "function" - } + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit_price", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "allow_partial", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "addStakeLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit_price", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "allow_partial", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "removeStakeLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, ]; \ No newline at end of file diff --git a/precompiles/src/solidity/stakingV2.abi b/precompiles/src/solidity/stakingV2.abi index 16adb1d8a8..20cc9c90fe 100644 --- a/precompiles/src/solidity/stakingV2.abi +++ b/precompiles/src/solidity/stakingV2.abi @@ -251,5 +251,71 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" - } + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit_price", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "allow_partial", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "addStakeLimit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limit_price", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "allow_partial", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "removeStakeLimit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, ] diff --git a/precompiles/src/solidity/stakingV2.sol b/precompiles/src/solidity/stakingV2.sol index dd033cfca8..202615af62 100644 --- a/precompiles/src/solidity/stakingV2.sol +++ b/precompiles/src/solidity/stakingV2.sol @@ -51,12 +51,12 @@ interface IStaking { ) external; /** - * @dev Moves a subtensor stake `amount` associated with the `hotkey` to a different hotkey + * @dev Moves a subtensor stake `amount` associated with the `hotkey` to a different hotkey * `destination_hotkey`. * * This function allows external accounts and contracts to move staked TAO from one hotkey to another, - * which effectively calls `move_stake` on the subtensor pallet with specified origin and destination - * hotkeys as parameters being the hashed address mappings of H160 sender address to Substrate ss58 + * which effectively calls `move_stake` on the subtensor pallet with specified origin and destination + * hotkeys as parameters being the hashed address mappings 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 * @@ -67,7 +67,7 @@ interface IStaking { * @param amount The amount to move in rao. * * Requirements: - * - `origin_hotkey` and `destination_hotkey` must be valid hotkeys registered on the network, ensuring + * - `origin_hotkey` and `destination_hotkey` must be valid hotkeys registered on the network, ensuring * that the stake is correctly attributed. */ function moveStake( @@ -79,12 +79,12 @@ interface IStaking { ) external; /** - * @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey + * @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey * `destination_coldkey`. * * 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 + * 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 * @@ -95,7 +95,7 @@ interface IStaking { * @param amount The amount to move in rao. * * Requirements: - * - `origin_hotkey` and `destination_hotkey` must be valid hotkeys registered on the network, ensuring + * - `origin_hotkey` and `destination_hotkey` must be valid hotkeys registered on the network, ensuring * that the stake is correctly attributed. */ function transferStake( @@ -194,4 +194,59 @@ interface IStaking { bytes32 hotkey, uint256 netuid ) external view returns (uint256); + + /** + * @dev Adds a subtensor stake `amount` associated with the `hotkey` within a price limit. + * + * This function allows external accounts and contracts to stake TAO into the subtensor pallet, + * which effectively calls `add_stake_limit` on the subtensor pallet with specified hotkey as a parameter + * and coldkey 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 hotkey The hotkey public key (32 bytes). + * @param amount The amount to stake in rao. + * @param limit_price The price limit to stake at in rao. Number of rao per alpha. + * @param allow_partial Whether to allow partial stake. + * @param netuid The subnet to stake to (uint256). + * + * Requirements: + * - `hotkey` must be a valid hotkey registered on the network, ensuring that the stake is + * correctly attributed. + */ + function addStakeLimit( + bytes32 hotkey, + uint256 amount, + uint256 limit_price, + bool allow_partial, + uint256 netuid + ) external payable; + + /** + * @dev Removes a subtensor stake `amount` from the specified `hotkey` within a price limit. + * + * This function allows external accounts and contracts to unstake TAO from the subtensor pallet, + * which effectively calls `remove_stake_limit` on the subtensor pallet with specified hotkey as a parameter + * and coldkey 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 hotkey The hotkey public key (32 bytes). + * @param amount The amount to unstake in alpha. + * @param limit_price The price limit to unstake at in rao. Number of rao per alpha. + * @param allow_partial Whether to allow partial unstake. + * @param netuid The subnet to stake to (uint256). + * + * Requirements: + * - `hotkey` must be a valid hotkey registered on the network, ensuring that the stake is + * correctly attributed. + * - The existing stake amount must be not lower than specified amount + */ + function removeStakeLimit( + bytes32 hotkey, + uint256 amount, + uint256 limit_price, + bool allow_partial, + uint256 netuid + ) external; } diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 8f797a7476..20a7bccf19 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -276,6 +276,56 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + + #[precompile::public("addStakeLimit(bytes32,uint256,uint256,bool,uint256)")] + fn add_stake_limit( + handle: &mut impl PrecompileHandle, + address: H256, + amount_rao: U256, + limit_price_rao: U256, + allow_partial: bool, + netuid: U256, + ) -> EvmResult<()> { + let account_id = handle.caller_account_id::(); + let amount_staked = amount_rao.unique_saturated_into(); + let limit_price = limit_price_rao.unique_saturated_into(); + let hotkey = R::AccountId::from(address.0); + let netuid = try_u16_from_u256(netuid)?; + let call = pallet_subtensor::Call::::add_stake_limit { + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + }; + + handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) + } + + #[precompile::public("removeStakeLimit(bytes32,uint256,uint256,bool,uint256)")] + fn remove_stake_limit( + handle: &mut impl PrecompileHandle, + address: H256, + amount_alpha: U256, + limit_price_rao: U256, + allow_partial: bool, + netuid: U256, + ) -> EvmResult<()> { + let account_id = handle.caller_account_id::(); + let hotkey = R::AccountId::from(address.0); + let netuid = try_u16_from_u256(netuid)?; + let amount_unstaked = amount_alpha.unique_saturated_into(); + let limit_price = limit_price_rao.unique_saturated_into(); + let call = pallet_subtensor::Call::::remove_stake_limit { + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + }; + + handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) + } } // Deprecated, exists for backward compatibility. From 38bb052a873f29c6c69c40d30528ef0f89514e66 Mon Sep 17 00:00:00 2001 From: open-junius Date: Tue, 29 Apr 2025 21:09:21 +0800 Subject: [PATCH 2/2] test passed --- .../test/staking.precompile.limit.test.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 evm-tests/test/staking.precompile.limit.test.ts diff --git a/evm-tests/test/staking.precompile.limit.test.ts b/evm-tests/test/staking.precompile.limit.test.ts new file mode 100644 index 0000000000..759aaecce2 --- /dev/null +++ b/evm-tests/test/staking.precompile.limit.test.ts @@ -0,0 +1,113 @@ +import * as assert from "assert"; +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate"; +import { devnet } from "@polkadot-api/descriptors"; +import { TypedApi } from "polkadot-api"; +import { + convertH160ToSS58, + convertPublicKeyToSs58, +} from "../src/address-utils"; +import { tao, raoToEth } from "../src/balance-math"; +import { + addNewSubnetwork, + addStake, + forceSetBalanceToEthAddress, + forceSetBalanceToSs58Address, + startCall, +} from "../src/subtensor"; +import { ethers } from "ethers"; +import { generateRandomEthersWallet } from "../src/utils"; +import { ISTAKING_V2_ADDRESS, IStakingV2ABI } from "../src/contracts/staking"; +import { log } from "console"; + +describe("Test staking precompile add remove limit methods", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const wallet1 = generateRandomEthersWallet(); + + let api: TypedApi; + + before(async () => { + api = await getDevnetApi(); + await forceSetBalanceToSs58Address( + api, + convertPublicKeyToSs58(hotkey.publicKey), + ); + await forceSetBalanceToSs58Address( + api, + convertPublicKeyToSs58(coldkey.publicKey), + ); + await forceSetBalanceToEthAddress(api, wallet1.address); + await addNewSubnetwork(api, hotkey, coldkey); + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; + await startCall(api, netuid, coldkey); + console.log("will test in subnet: ", netuid); + }); + + it("Staker add limit", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; + let ss58Address = convertH160ToSS58(wallet1.address); + + const alpha = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid, + ); + + const contract = new ethers.Contract( + ISTAKING_V2_ADDRESS, + IStakingV2ABI, + wallet1, + ); + + const tx = await contract.addStakeLimit( + hotkey.publicKey, + tao(2000), + tao(1000), + true, + netuid, + ); + await tx.wait(); + + const alphaAfterAddStake = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid, + ); + + assert.ok(alphaAfterAddStake > alpha); + }); + + it("Staker remove limit", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; + let ss58Address = convertH160ToSS58(wallet1.address); + + const alpha = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid, + ); + + const contract = new ethers.Contract( + ISTAKING_V2_ADDRESS, + IStakingV2ABI, + wallet1, + ); + + const tx = await contract.removeStakeLimit( + hotkey.publicKey, + tao(100), + tao(1), + true, + netuid, + ); + await tx.wait(); + + const alphaAfterRemoveStake = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid, + ); + + assert.ok(alphaAfterRemoveStake < alpha); + }); +});