diff --git a/evm-tests/src/contracts/staking.ts b/evm-tests/src/contracts/staking.ts index 7b9e671c23..4b48fd7d8d 100644 --- a/evm-tests/src/contracts/staking.ts +++ b/evm-tests/src/contracts/staking.ts @@ -12,7 +12,7 @@ export const IStakingABI = [ ], name: "addProxy", outputs: [], - stateMutability: "nonpayable", + stateMutability: "payable", type: "function", }, { @@ -43,7 +43,7 @@ export const IStakingABI = [ ], name: "removeProxy", outputs: [], - stateMutability: "nonpayable", + stateMutability: "payable", type: "function", }, { @@ -95,7 +95,7 @@ export const IStakingABI = [ ], name: "removeStake", outputs: [], - stateMutability: "nonpayable", + stateMutability: "payable", type: "function", }, ]; @@ -111,7 +111,7 @@ export const IStakingV2ABI = [ ], "name": "addProxy", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -275,7 +275,7 @@ export const IStakingV2ABI = [ ], "name": "removeProxy", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -298,7 +298,7 @@ export const IStakingV2ABI = [ ], "name": "removeStake", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -331,7 +331,7 @@ export const IStakingV2ABI = [ ], "name": "addStakeLimit", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -364,7 +364,7 @@ export const IStakingV2ABI = [ ], "name": "removeStakeLimit", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -382,7 +382,7 @@ export const IStakingV2ABI = [ ], "name": "removeStakeFull", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -405,7 +405,30 @@ export const IStakingV2ABI = [ ], "name": "removeStakeFullLimit", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "burnAlpha", + "outputs": [], + "stateMutability": "payable", "type": "function" } ]; \ No newline at end of file diff --git a/evm-tests/test/staking.precompile.burn-alpha.test.ts b/evm-tests/test/staking.precompile.burn-alpha.test.ts new file mode 100644 index 0000000000..825587602e --- /dev/null +++ b/evm-tests/test/staking.precompile.burn-alpha.test.ts @@ -0,0 +1,136 @@ +import * as assert from "assert"; +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" +import { tao } from "../src/balance-math" +import { ethers } from "ethers" +import { generateRandomEthersWallet } from "../src/utils" +import { convertH160ToPublicKey } from "../src/address-utils" +import { + forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister, + startCall, +} from "../src/subtensor" +import { ISTAKING_V2_ADDRESS, IStakingV2ABI } from "../src/contracts/staking" + +describe("Test staking precompile burn alpha", () => { + // init eth part + const wallet1 = generateRandomEthersWallet(); + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + + let api: TypedApi + + before(async () => { + // init variables got from await and async + api = await getDevnetApi() + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToEthAddress(api, wallet1.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) + }) + + it("Can burn alpha after adding stake", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + + // First add some stake + let stakeBalance = tao(50) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + const addStakeTx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid) + await addStakeTx.wait() + + // Get stake before burning + const stakeBefore = BigInt(await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid)) + + console.log("Stake before burn:", stakeBefore) + assert.ok(stakeBefore > BigInt(0), "Should have stake before burning") + + // Burn some alpha (burn 20 TAO worth) + let burnAmount = tao(20) + const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid) + await burnTx.wait() + + // Get stake after burning + const stakeAfter = BigInt(await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid)) + + console.log("Stake after burn:", stakeAfter) + + // Verify that stake decreased by burn amount + assert.ok(stakeAfter < stakeBefore, "Stake should decrease after burning") + // assert.strictEqual(stakeBefore - stakeAfter, burnAmount, "Stake should decrease by exactly burn amount") + }) + + it("Cannot burn more alpha than staked", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + + // Get current stake + const currentStake = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + convertH160ToSS58(wallet1.address), + netuid + ) + + // Try to burn more than staked + let burnAmount = currentStake + tao(10000) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + + try { + const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid) + await burnTx.wait() + assert.fail("Transaction should have failed - cannot burn more than staked"); + } catch (error) { + // Transaction failed as expected + console.log("Correctly failed to burn more than staked amount") + assert.ok(true, "Burning more than staked should fail"); + } + }) + + it("Cannot burn alpha from non-existent subnet", async () => { + // wrong netuid + let netuid = 12345; + let burnAmount = tao(10) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + + try { + const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid) + await burnTx.wait() + assert.fail("Transaction should have failed - subnet doesn't exist"); + } catch (error) { + // Transaction failed as expected + console.log("Correctly failed to burn from non-existent subnet") + assert.ok(true, "Burning from non-existent subnet should fail"); + } + }) + + it("Cannot burn zero alpha", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + + // First add some stake for this test + let stakeBalance = tao(10) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + const addStakeTx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid) + await addStakeTx.wait() + + // Try to burn zero amount + let burnAmount = BigInt(0) + + try { + const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid) + await burnTx.wait() + assert.fail("Transaction should have failed - cannot burn zero amount"); + } catch (error) { + // Transaction failed as expected + console.log("Correctly failed to burn zero amount") + assert.ok(true, "Burning zero amount should fail"); + } + }) +}) + diff --git a/precompiles/src/solidity/stakingV2.abi b/precompiles/src/solidity/stakingV2.abi index 2c936898ab..40a5acc1d9 100644 --- a/precompiles/src/solidity/stakingV2.abi +++ b/precompiles/src/solidity/stakingV2.abi @@ -9,7 +9,7 @@ ], "name": "addProxy", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -226,7 +226,7 @@ ], "name": "moveStake", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -239,7 +239,7 @@ ], "name": "removeProxy", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -262,7 +262,7 @@ ], "name": "removeStake", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -280,7 +280,7 @@ ], "name": "removeStakeFull", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -303,7 +303,7 @@ ], "name": "removeStakeFullLimit", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -336,7 +336,7 @@ ], "name": "removeStakeLimit", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -369,7 +369,30 @@ ], "name": "transferStake", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "burnAlpha", + "outputs": [], + "stateMutability": "payable", "type": "function" } ] diff --git a/precompiles/src/solidity/stakingV2.sol b/precompiles/src/solidity/stakingV2.sol index c8c39761ad..fefca82fd9 100644 --- a/precompiles/src/solidity/stakingV2.sol +++ b/precompiles/src/solidity/stakingV2.sol @@ -48,7 +48,7 @@ interface IStaking { bytes32 hotkey, uint256 amount, uint256 netuid - ) external; + ) external payable; /** * @dev Moves a subtensor stake `amount` associated with the `hotkey` to a different hotkey @@ -76,7 +76,7 @@ interface IStaking { uint256 origin_netuid, uint256 destination_netuid, uint256 amount - ) external; + ) external payable; /** * @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey @@ -104,7 +104,7 @@ interface IStaking { uint256 origin_netuid, uint256 destination_netuid, uint256 amount - ) external; + ) external payable; /** * @dev Returns the amount of RAO staked by the coldkey. @@ -156,14 +156,14 @@ interface IStaking { * * @param delegate The public key (32 bytes) of the delegate. */ - function addProxy(bytes32 delegate) external; + function addProxy(bytes32 delegate) external payable; /** * @dev Removes staking proxy account. * * @param delegate The public key (32 bytes) of the delegate. */ - function removeProxy(bytes32 delegate) external; + function removeProxy(bytes32 delegate) external payable; /** * @dev Returns the validators that have staked alpha under a hotkey. @@ -258,7 +258,7 @@ interface IStaking { uint256 limit_price, bool allow_partial, uint256 netuid - ) external; + ) external payable; /** * @dev Removes all stake from a hotkey on a subnet with a price limit. @@ -270,7 +270,7 @@ interface IStaking { * @param hotkey The hotkey public key (32 bytes). * @param netuid The subnet to remove stake from (uint256). */ - function removeStakeFull(bytes32 hotkey, uint256 netuid) external; + function removeStakeFull(bytes32 hotkey, uint256 netuid) external payable; /** * @dev Removes all stake from a hotkey on a subnet with a price limit. @@ -287,5 +287,27 @@ interface IStaking { bytes32 hotkey, uint256 netuid, uint256 limitPrice - ) external; + ) external payable; + + /** + * @dev Burns alpha tokens from the specified hotkey's stake on a subnet. + * + * This function allows external accounts and contracts to permanently burn (destroy) alpha tokens + * from their stake on a specified hotkey and subnet. The burned tokens are removed from circulation + * and cannot be recovered. + * + * @param hotkey The hotkey public key (32 bytes). + * @param amount The amount of alpha to burn (uint256). + * @param netuid The subnet to burn from (uint256). + * + * Requirements: + * - `hotkey` must be a valid hotkey registered on the network. + * - The caller must have sufficient alpha staked to the specified hotkey on the subnet. + * - `amount` must be greater than zero and not exceed the staked amount. + */ + function burnAlpha( + bytes32 hotkey, + uint256 amount, + uint256 netuid + ) external payable; } diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index b896e61e33..35daaf4f47 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -217,6 +217,27 @@ where handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } + #[precompile::public("burnAlpha(bytes32,uint256,uint256)")] + #[precompile::payable] + fn burn_alpha( + handle: &mut impl PrecompileHandle, + hotkey: H256, + amount: U256, + netuid: U256, + ) -> EvmResult<()> { + let account_id = handle.caller_account_id::(); + let hotkey = R::AccountId::from(hotkey.0); + let netuid = try_u16_from_u256(netuid)?; + let amount: u64 = amount.unique_saturated_into(); + let call = pallet_subtensor::Call::::burn_alpha { + hotkey, + amount: amount.into(), + netuid: netuid.into(), + }; + + handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) + } + #[precompile::public("getTotalColdkeyStake(bytes32)")] #[precompile::view] fn get_total_coldkey_stake( diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 389a01a983..de8ac4e6e5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -220,7 +220,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 329, + spec_version: 330, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,