diff --git a/contract-tests/test/sr25519.precompile.verify.test.ts b/contract-tests/test/sr25519.precompile.verify.test.ts deleted file mode 100644 index 234638f195..0000000000 --- a/contract-tests/test/sr25519.precompile.verify.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ISr25519VERIFY_ADDRESS, ISr25519VerifyABI, ETH_LOCAL_URL } from '../src/config' -import { getPublicClient } from "../src/utils"; -import { toHex, toBytes, keccak256, PublicClient } from 'viem' -import { Keyring } from "@polkadot/keyring"; -import * as assert from "assert"; - -describe("Verfication of sr25519 signature", () => { - // init eth part - let ethClient: PublicClient; - - before(async () => { - ethClient = await getPublicClient(ETH_LOCAL_URL); - }); - - it("Verification of sr25519 works", async () => { - const keyring = new Keyring({ type: "sr25519" }); - const alice = keyring.addFromUri("//Alice"); - - ////////////////////////////////////////////////////////////////////// - // Generate a signature - - // Your message to sign - const message = "Sign this message"; - const messageU8a = new TextEncoder().encode(message); - const messageHex = toHex(messageU8a); // Convert message to hex string - const messageHash = keccak256(messageHex); // Hash the message to fit into bytes32 - console.log(`messageHash = ${messageHash}`); - const hashedMessageBytes = toBytes(messageHash); - console.log(`hashedMessageBytes = ${hashedMessageBytes}`); - - // Sign the message - const signature = await alice.sign(hashedMessageBytes); - console.log(`Signature: ${toHex(signature)}`); - - // Verify the signature locally - const isValid = alice.verify( - hashedMessageBytes, - signature, - alice.publicKey - ); - console.log(`Is the signature valid? ${isValid}`); - - ////////////////////////////////////////////////////////////////////// - // Verify the signature using the precompile contract - - const publicKeyBytes = toHex(alice.publicKey); - console.log(`publicKeyBytes = ${publicKeyBytes}`); - - // Split signture into Commitment (R) and response (s) - let r = signature.slice(0, 32); // Commitment, a.k.a. "r" - first 32 bytes - let s = signature.slice(32, 64); // Response, a.k.a. "s" - second 32 bytes - let rBytes = toHex(r); - let sBytes = toHex(s); - - const isPrecompileValid = await ethClient.readContract({ - address: ISr25519VERIFY_ADDRESS, - abi: ISr25519VerifyABI, - functionName: "verify", - args: [messageHash, - publicKeyBytes, - rBytes, - sBytes] - - }); - - console.log( - `Is the signature valid according to the smart contract? ${isPrecompileValid}` - ); - assert.equal(isPrecompileValid, true) - - ////////////////////////////////////////////////////////////////////// - // Verify the signature for bad data using the precompile contract - - let brokenHashedMessageBytes = hashedMessageBytes; - brokenHashedMessageBytes[0] = (brokenHashedMessageBytes[0] + 1) % 0xff; - const brokenMessageHash = toHex(brokenHashedMessageBytes); - console.log(`brokenMessageHash = ${brokenMessageHash}`); - - const isPrecompileValidBadData = await ethClient.readContract({ - address: ISr25519VERIFY_ADDRESS, - abi: ISr25519VerifyABI, - functionName: "verify", - args: [brokenMessageHash, - publicKeyBytes, - rBytes, - sBytes] - - }); - - console.log( - `Is the signature valid according to the smart contract for broken data? ${isPrecompileValidBadData}` - ); - assert.equal(isPrecompileValidBadData, false) - - ////////////////////////////////////////////////////////////////////// - // Verify the bad signature for good data using the precompile contract - - let brokenR = r; - brokenR[0] = (brokenR[0] + 1) % 0xff; - rBytes = toHex(r); - const isPrecompileValidBadSignature = await ethClient.readContract({ - address: ISr25519VERIFY_ADDRESS, - abi: ISr25519VerifyABI, - functionName: "verify", - args: [messageHash, - publicKeyBytes, - rBytes, - sBytes] - - }); - - console.log( - `Is the signature valid according to the smart contract for broken signature? ${isPrecompileValidBadSignature}` - ); - assert.equal(isPrecompileValidBadSignature, false) - - }); -}); \ No newline at end of file diff --git a/contract-tests/test/staking.precompile.add-remove.test.ts b/contract-tests/test/staking.precompile.add-remove.test.ts deleted file mode 100644 index 9eef7d4dbf..0000000000 --- a/contract-tests/test/staking.precompile.add-remove.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -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, - getStake, -} 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 neuron precompile add remove stake", () => { - // 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 - - // 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) - }) - - it("Can add stake", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - // ETH unit - let stakeBalance = raoToEth(tao(20)) - const stakeBefore = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) - const contract = new ethers.Contract(ISTAKING_ADDRESS, IStakingABI, wallet1); - const tx = await contract.addStake(hotkey.publicKey, netuid, { value: stakeBalance.toString() }) - await tx.wait() - - const stakeFromContract = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) - ); - - assert.ok(stakeFromContract > stakeBefore) - const stakeAfter = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) - assert.ok(stakeAfter > stakeBefore) - }) - - it("Can add stake V2", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - // the unit in V2 is RAO, not ETH - let stakeBalance = tao(20) - const stakeBefore = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) - const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); - const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid) - await tx.wait() - - const stakeFromContract = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) - ); - - assert.ok(stakeFromContract > stakeBefore) - const stakeAfter = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) - assert.ok(stakeAfter > stakeBefore) - }) - - it("Can not add stake if subnet doesn't exist", async () => { - // wrong netuid - let netuid = 12345; - let stakeBalance = raoToEth(tao(20)) - const stakeBefore = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) - const contract = new ethers.Contract(ISTAKING_ADDRESS, IStakingABI, wallet1); - try { - const tx = await contract.addStake(hotkey.publicKey, netuid, { value: stakeBalance.toString() }) - await tx.wait() - assert.fail("Transaction should have failed"); - } catch (error) { - // Transaction failed as expected - } - - const stakeFromContract = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) - ); - assert.equal(stakeFromContract, stakeBefore) - const stakeAfter = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) - assert.equal(stakeAfter, stakeBefore) - }); - - it("Can not add stake V2 if subnet doesn't exist", async () => { - // wrong netuid - let netuid = 12345; - // the unit in V2 is RAO, not ETH - let stakeBalance = tao(20) - const stakeBefore = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) - const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); - - try { - const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid); - await tx.wait(); - assert.fail("Transaction should have failed"); - } catch (error) { - // Transaction failed as expected - } - - const stakeFromContract = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) - ); - assert.equal(stakeFromContract, stakeBefore) - const stakeAfter = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) - assert.equal(stakeAfter, stakeBefore) - }) - - it("Can get stake via contract read method", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - - // TODO need check how to pass bytes32 as parameter of readContract - // const value = await publicClient.readContract({ - // address: ISTAKING_ADDRESS, - // abi: IStakingABI, - // functionName: "getStake", - // args: [hotkey.publicKey, // Convert to bytes32 format - // convertH160ToPublicKey(wallet1.address), - // netuid] - // }) - // if (value === undefined || value === null) { - // throw new Error("value of getStake from contract is undefined") - // } - // const intValue = BigInt(value.toString()) - - const contractV1 = new ethers.Contract(ISTAKING_ADDRESS, IStakingABI, wallet1); - const stakeFromContractV1 = BigInt( - await contractV1.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) - ); - - const contractV2 = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); - // unit from contract V2 is RAO, not ETH - const stakeFromContractV2 = Number( - await contractV2.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) - ); - - assert.equal(stakeFromContractV1, tao(stakeFromContractV2)) - - const totalColdkeyStakeOnSubnet = Number( - await contractV2.getTotalColdkeyStakeOnSubnet(convertH160ToPublicKey(wallet1.address), netuid) - ); - - // check the value is not undefined and is greater than or equal to the stake from contract V2 - assert.ok(totalColdkeyStakeOnSubnet != undefined) - // is greater than or equal to the stake from contract V2 because of emission - assert.ok(totalColdkeyStakeOnSubnet >= stakeFromContractV2) - - }) - - it("Can remove stake", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - const contract = new ethers.Contract( - ISTAKING_ADDRESS, - IStakingABI, - wallet1 - ); - - const stakeBeforeRemove = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) - ); - - let stakeBalance = raoToEth(tao(10)) - const tx = await contract.removeStake(hotkey.publicKey, stakeBalance, netuid) - await tx.wait() - - const stakeAfterRemove = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) - ); - assert.ok(stakeAfterRemove < stakeBeforeRemove) - - }) - - it("Can remove stake V2", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet2 - ); - - const stakeBeforeRemove = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) - ); - - let stakeBalance = tao(10) - const tx = await contract.removeStake(hotkey.publicKey, stakeBalance, netuid) - await tx.wait() - - const stakeAfterRemove = BigInt( - await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) - ); - - assert.ok(stakeAfterRemove < stakeBeforeRemove) - }) - - it("Can add/remove proxy", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - // add/remove are done in a single test case, because we can't use the same private/public key - // between substrate and EVM, but to test the remove part, we must predefine the proxy first. - // it makes `remove` being dependent on `add`, because we should use `addProxy` from contract - // to prepare the proxy for `removeProxy` testing - the proxy is specified for the - // caller/origin. - - // first, check we don't have proxies - const ss58Address = convertH160ToSS58(wallet1.address); - // the result include two items array, first one is delegate info, second one is balance - const initProxies = await api.query.Proxy.Proxies.getValue(ss58Address); - assert.equal(initProxies[0].length, 0); - - // intialize the contract - const contract = new ethers.Contract( - ISTAKING_ADDRESS, - IStakingABI, - wallet1 - ); - - // test "add" - let tx = await contract.addProxy(proxy.publicKey); - await tx.wait(); - - const proxiesAfterAdd = await api.query.Proxy.Proxies.getValue(ss58Address); - - assert.equal(proxiesAfterAdd[0][0].delegate, convertPublicKeyToSs58(proxy.publicKey)) - - let stakeBefore = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid - ) - - const call = api.tx.SubtensorModule.add_stake({ - hotkey: convertPublicKeyToSs58(hotkey.publicKey), - netuid: netuid, - amount_staked: tao(1) - }) - await sendProxyCall(api, call.decodedCall, ss58Address, proxy) - - let stakeAfter = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid - ) - - assert.ok(stakeAfter > stakeBefore) - // test "remove" - tx = await contract.removeProxy(proxy.publicKey); - await tx.wait(); - - const proxiesAfterRemove = await api.query.Proxy.Proxies.getValue(ss58Address); - assert.equal(proxiesAfterRemove[0].length, 0) - }); - - it("Can add/remove proxy V2", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - // add/remove are done in a single test case, because we can't use the same private/public key - // between substrate and EVM, but to test the remove part, we must predefine the proxy first. - // it makes `remove` being dependent on `add`, because we should use `addProxy` from contract - // to prepare the proxy for `removeProxy` testing - the proxy is specified for the - // caller/origin. - - // first, check we don't have proxies - const ss58Address = convertH160ToSS58(wallet1.address); - // the result include two items array, first one is delegate info, second one is balance - const initProxies = await api.query.Proxy.Proxies.getValue(ss58Address); - assert.equal(initProxies[0].length, 0); - - // intialize the contract - // const signer = new ethers.Wallet(fundedEthWallet.privateKey, provider); - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet1 - ); - - // test "add" - let tx = await contract.addProxy(proxy.publicKey); - await tx.wait(); - - const proxiesAfterAdd = await api.query.Proxy.Proxies.getValue(ss58Address); - - assert.equal(proxiesAfterAdd[0][0].delegate, convertPublicKeyToSs58(proxy.publicKey)) - - let stakeBefore = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid - ) - - const call = api.tx.SubtensorModule.add_stake({ - hotkey: convertPublicKeyToSs58(hotkey.publicKey), - netuid: netuid, - amount_staked: tao(1) - }) - - await sendProxyCall(api, call.decodedCall, ss58Address, proxy) - - let stakeAfter = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid - ) - - assert.ok(stakeAfter > stakeBefore) - // test "remove" - tx = await contract.removeProxy(proxy.publicKey); - await tx.wait(); - - const proxiesAfterRemove = await api.query.Proxy.Proxies.getValue(ss58Address); - assert.equal(proxiesAfterRemove[0].length, 0) - }); -}); diff --git a/contract-tests/test/staking.precompile.approval.test.ts b/contract-tests/test/staking.precompile.approval.test.ts deleted file mode 100644 index 94161696cd..0000000000 --- a/contract-tests/test/staking.precompile.approval.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -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, - getStake, -} 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 getStake(api, 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 getStake(api, 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( - wallet1.address, // source - wallet2.address, // destination - 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( - wallet1.address, // source - wallet2.address, // spender - stakeNetuid, - ) - ); - assert.equal(allowance, expectedAllowance, "default allowance should be 0"); - } - - { - const tx = await contract.approve( - wallet2.address, // spender - stakeNetuid, - tao(10) - ) - await tx.wait(); - - expectedAllowance += BigInt(tao(10)); - - let allowance = BigInt( - await contract.allowance( - wallet1.address, // source - wallet2.address, // spender - 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( - wallet1.address, // source - wallet2.address, // destination - hotkey.publicKey, - stakeNetuid, - stakeNetuid, - tao(5) - ) - await tx.wait(); - - expectedAllowance -= BigInt(tao(5)); - - { - let allowance = BigInt( - await contract.allowance( - wallet1.address, // source - wallet2.address, // spender - 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( - wallet1.address, // source - wallet2.address, // destination - 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( - wallet2.address, // spender - stakeNetuid, - tao(10) - ) - await tx.wait(); - - expectedAllowance += BigInt(tao(10)); - - let allowance = BigInt( - await contract.allowance( - wallet1.address, // source - wallet2.address, // spender - stakeNetuid, - ) - ); - assert.equal(allowance, expectedAllowance, "allowance have been increased correctly"); - } - - { - const tx = await contract.decreaseAllowance( - wallet2.address, // spender - stakeNetuid, - tao(2) - ) - await tx.wait(); - - expectedAllowance -= BigInt(tao(2)); - - let allowance = BigInt( - await contract.allowance( - wallet1.address, // source - wallet2.address, // spender - stakeNetuid, - ) - ); - assert.equal(allowance, expectedAllowance, "allowance have been decreased correctly"); - } - - { - const tx = await contract.approve( - wallet2.address, // spender - stakeNetuid, - 0 - ) - await tx.wait(); - - expectedAllowance = BigInt(0); - - let allowance = BigInt( - await contract.allowance( - wallet1.address, // source - wallet2.address, // spender - stakeNetuid, - ) - ); - assert.equal(allowance, expectedAllowance, "allowance have been overwritten correctly"); - } - }) -}) diff --git a/contract-tests/test/staking.precompile.burn-alpha.test.ts b/contract-tests/test/staking.precompile.burn-alpha.test.ts deleted file mode 100644 index f98c988b52..0000000000 --- a/contract-tests/test/staking.precompile.burn-alpha.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -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, - getStake, -} 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 getStake( - api, - 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/contract-tests/test/staking.precompile.full-limit.test.ts b/contract-tests/test/staking.precompile.full-limit.test.ts deleted file mode 100644 index faf09d65fd..0000000000 --- a/contract-tests/test/staking.precompile.full-limit.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -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, - getStake, - 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(); - const wallet2 = 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 forceSetBalanceToEthAddress(api, wallet2.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); - }); - - describe("Add limit then remove stake with limit price", () => { - it("Staker add limit for wallet 1", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; - let ss58Address = convertH160ToSS58(wallet1.address); - - const alpha = await getStake( - api, - 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 getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - assert.ok(alphaAfterAddStake > alpha); - }); - - it("Staker remove stake with limit price", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; - let ss58Address = convertH160ToSS58(wallet1.address); - - const alpha = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet1, - ); - - const tx = await contract.removeStakeFullLimit( - hotkey.publicKey, - netuid, - 90_000_000, - ); - await tx.wait(); - - const alphaAfterRemoveStake = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - assert.ok(alphaAfterRemoveStake < alpha); - }); - }); - - describe("Add limit then remove stake full", () => { - - it("Staker add limit for wallet 2", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; - let ss58Address = convertH160ToSS58(wallet2.address); - - const alpha = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet2, - ); - - const tx = await contract.addStakeLimit( - hotkey.publicKey, - tao(2000), - tao(1000), - true, - netuid, - ); - await tx.wait(); - - const alphaAfterAddStake = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - assert.ok(alphaAfterAddStake > alpha); - }); - - it("Staker remove stake with full", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; - let ss58Address = convertH160ToSS58(wallet2.address); - - const alpha = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet2, - ); - - const tx = await contract.removeStakeFull( - hotkey.publicKey, - netuid, - ); - await tx.wait(); - - const alphaAfterRemoveStake = await getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - assert.ok(alphaAfterRemoveStake < alpha); - }); - }); -}); diff --git a/contract-tests/test/staking.precompile.limit.test.ts b/contract-tests/test/staking.precompile.limit.test.ts deleted file mode 100644 index eff1394911..0000000000 --- a/contract-tests/test/staking.precompile.limit.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -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, - getStake, - 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 getStake( - api, - 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 getStake( - api, - 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 getStake( - api, - 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 getStake( - api, - convertPublicKeyToSs58(hotkey.publicKey), - ss58Address, - netuid, - ); - - assert.ok(alphaAfterRemoveStake < alpha); - }); -}); diff --git a/contract-tests/test/staking.precompile.stake-get.test.ts b/contract-tests/test/staking.precompile.stake-get.test.ts deleted file mode 100644 index 4730e310d9..0000000000 --- a/contract-tests/test/staking.precompile.stake-get.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as assert from "assert"; -import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" -import { devnet } from "@polkadot-api/descriptors" -import { TypedApi } from "polkadot-api"; -import { convertPublicKeyToSs58 } from "../src/address-utils" -import { tao } from "../src/balance-math" -import { - forceSetBalanceToSs58Address, addNewSubnetwork, addStake, - 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 get 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 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 receives rewards", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - - await addStake(api, netuid, convertPublicKeyToSs58(hotkey.publicKey), tao(1), coldkey) - - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet1 - ); - - const stake = BigInt( - await contract.getStake(hotkey.publicKey, coldkey.publicKey, netuid) - ); - - // validator returned as bigint now. - const validators = - await contract.getAlphaStakedValidators(hotkey.publicKey, netuid) - - const alpha = BigInt( - await contract.getTotalAlphaStaked(hotkey.publicKey, netuid) - ); - assert.ok(stake > 0) - assert.equal(validators.length, 1) - assert.ok(alpha > 0) - - }) - - it("Get nominator min required stake", async () => { - const contract = new ethers.Contract( - ISTAKING_V2_ADDRESS, - IStakingV2ABI, - wallet1 - ); - - const stake = await contract.getNominatorMinRequiredStake() - const stakeOnChain = await api.query.SubtensorModule.NominatorMinRequiredStake.getValue() - - assert.ok(stake !== undefined) - assert.equal(stake.toString(), stakeOnChain.toString()) - - }) -}) diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index 452d8cf6a7..f1482b5262 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -594,3 +594,9 @@ pub(crate) fn alpha_price_to_evm(price: U96F32) -> U256 { .expect("runtime balance conversion should work for alpha price") .into_u256() } + +pub(crate) fn substrate_to_evm(amount: u64) -> U256 { + ::BalanceConverter::into_evm_balance(amount.into()) + .expect("runtime balance conversion should work") + .into_u256() +} diff --git a/precompiles/src/sr25519.rs b/precompiles/src/sr25519.rs index 054948d524..324bd7abca 100644 --- a/precompiles/src/sr25519.rs +++ b/precompiles/src/sr25519.rs @@ -55,3 +55,82 @@ where Ok((ExitSucceed::Returned, buf.to_vec())) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::mock::{ + AccountId, abi_word, addr_from_index, new_test_ext, precompiles, selector_u32, + }; + use precompile_utils::solidity::encode_with_selector; + use precompile_utils::testing::PrecompileTesterExt; + use sp_core::{H256, Pair, U256, sr25519}; + + #[test] + fn sr25519_precompile_verifies_valid_and_invalid_signatures() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(Sr25519Verify::::INDEX); + + let pair = sr25519::Pair::from_seed(&[1u8; 32]); + let message = [7u8; 32]; + let signature = pair.sign(&message); + let public_key = pair.public(); + let broken_message = [8u8; 32]; + let mut broken_signature = signature.0; + broken_signature[0] ^= 1; + let broken_signature = sr25519::Signature::from_raw(broken_signature); + + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(message), + H256::from(public_key.0), + H256::from_slice(&signature.0[..32]), + H256::from_slice(&signature.0[32..]), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::one())); + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(broken_message), + H256::from(public_key.0), + H256::from_slice(&signature.0[..32]), + H256::from_slice(&signature.0[32..]), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::zero())); + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(message), + H256::from(public_key.0), + H256::from_slice(&broken_signature.0[..32]), + H256::from_slice(&broken_signature.0[32..]), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::zero())); + }); + } +} diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index f64f9ca319..525b810cfc 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -917,3 +917,1143 @@ fn try_u64_from_u256(value: U256) -> Result { exit_status: ExitError::Other("the value is outside of u64 bounds".into()), }) } + +#[cfg(test)] +mod tests { + #![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing + )] + + use super::*; + use crate::PrecompileExt; + use crate::mock::{ + AccountId, Proxy, Runtime, RuntimeCall, RuntimeOrigin, addr_from_index, assert_static_call, + execute_precompile, new_test_ext, precompiles, selector_u32, substrate_to_evm, + }; + use pallet_evm::AddressMapping; + use precompile_utils::solidity::{encode_return_value, encode_with_selector}; + use precompile_utils::testing::PrecompileTesterExt; + use sp_core::{H160, H256}; + use substrate_fixed::types::U64F64; + use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + + const TEST_NETUID_U16: u16 = 1; + const INVALID_NETUID_U16: u16 = 12_345; + const TEMPO: u16 = 100; + const RESERVE_TAO: u64 = 200_000_000_000; + const RESERVE_ALPHA: u64 = 100_000_000_000; + const INITIAL_STAKE_RAO: u64 = 20_000_000_000; + const REMOVE_STAKE_RAO: u64 = 10_000_000_000; + const PROXY_STAKE_RAO: u64 = 1_000_000_000; + const COLDKEY_BALANCE: u64 = 100_000_000_000; + const APPROVED_ALLOWANCE_RAO: u64 = 10_000_000_000; + const TRANSFERRED_ALLOWANCE_RAO: u64 = 5_000_000_000; + const ALLOWANCE_DECREASE_RAO: u64 = 2_000_000_000; + + fn setup_staking_subnet() -> NetUid { + let netuid = NetUid::from(TEST_NETUID_U16); + pallet_subtensor::Pallet::::init_new_network(netuid, TEMPO); + pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); + pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); + pallet_subtensor::FirstEmissionBlockNumber::::insert(netuid, 0); + pallet_subtensor::SubtokenEnabled::::insert(netuid, true); + pallet_subtensor::BurnHalfLife::::insert(netuid, 1); + pallet_subtensor::BurnIncreaseMult::::insert(netuid, U64F64::from_num(1)); + pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE_TAO)); + pallet_subtensor::SubnetAlphaIn::::insert( + netuid, + AlphaBalance::from(RESERVE_ALPHA), + ); + netuid + } + + fn mapped_account(address: H160) -> AccountId { + ::AddressMapping::into_account_id(address) + } + + fn fund_account(account: &AccountId, amount: u64) { + pallet_subtensor::Pallet::::add_balance_to_coldkey_account(account, amount.into()); + } + + fn hotkey() -> AccountId { + AccountId::from([0x11; 32]) + } + + fn delegate() -> AccountId { + AccountId::from([0x22; 32]) + } + + fn ensure_hotkey_exists(hotkey: &AccountId) { + pallet_subtensor::Owner::::insert(hotkey, hotkey.clone()); + } + + fn stake_for(hotkey: &AccountId, coldkey: &AccountId, netuid: NetUid) -> u64 { + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, coldkey, netuid, + ) + .into() + } + + fn total_coldkey_stake_on_subnet(coldkey: &AccountId, netuid: NetUid) -> u64 { + pallet_subtensor::Pallet::::get_total_stake_for_coldkey_on_subnet(coldkey, netuid) + .into() + } + + fn add_stake_v1(caller: H160, hotkey: &AccountId, netuid: u16, amount_rao: u64) { + ensure_hotkey_exists(hotkey); + fund_account(&StakingPrecompile::::account_id(), amount_rao); + + let result = execute_precompile( + &precompiles::>(), + addr_from_index(StakingPrecompile::::INDEX), + caller, + encode_with_selector( + selector_u32("addStake(bytes32,uint256)"), + (H256::from_slice(hotkey.as_ref()), U256::from(netuid)), + ), + substrate_to_evm(amount_rao), + ) + .expect("staking v1 add stake should route to the precompile"); + + assert!(result.is_ok()); + } + + fn add_stake_v2(caller: H160, hotkey: &AccountId, netuid: u16, amount_rao: u64) { + ensure_hotkey_exists(hotkey); + precompiles::>() + .prepare_test( + caller, + addr_from_index(StakingPrecompileV2::::INDEX), + encode_with_selector( + selector_u32("addStake(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(amount_rao), + U256::from(netuid), + ), + ), + ) + .execute_returns(()); + } + + fn assert_proxy_effects(caller: H160, netuid: NetUid) { + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let delegate = delegate(); + + ensure_hotkey_exists(&hotkey); + + let proxies = pallet_subtensor_proxy::Proxies::::get(&caller_account).0; + assert_eq!(proxies.len(), 1); + assert_eq!(proxies[0].delegate, delegate); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + let proxied_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: hotkey.clone(), + netuid, + amount_staked: PROXY_STAKE_RAO.into(), + }); + let proxy_result = Proxy::proxy( + RuntimeOrigin::signed(delegate.clone()), + caller_account.clone().into(), + Some(ProxyType::Staking), + Box::new(proxied_call), + ); + assert!(proxy_result.is_ok()); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert!(stake_after > stake_before); + } + + fn setup_approval_state() -> (NetUid, H160, H160, AccountId, AccountId, AccountId) { + let netuid = setup_staking_subnet(); + let source = addr_from_index(0x2001); + let spender = addr_from_index(0x2002); + let source_account = mapped_account(source); + let spender_account = mapped_account(spender); + let hotkey = hotkey(); + + fund_account(&source_account, COLDKEY_BALANCE); + add_stake_v2(source, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + pallet_subtensor::StakingOperationRateLimiter::::remove(( + hotkey.clone(), + source_account.clone(), + netuid, + )); + + ( + netuid, + source, + spender, + source_account, + spender_account, + hotkey, + ) + } + + fn assert_allowance(source: H160, spender: H160, caller: H160, expected: U256) { + assert_static_call( + &precompiles::>(), + caller, + addr_from_index(StakingPrecompileV2::::INDEX), + encode_with_selector( + selector_u32("allowance(address,address,uint256)"), + ( + precompile_utils::solidity::codec::Address(source), + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + ), + ), + expected, + ); + } + + #[test] + fn staking_precompile_v1_add_stake_and_reads_match_runtime_state() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1001); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + add_stake_v1(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert!(stake_after > stake_before); + + assert_static_call( + &precompiles::>(), + caller, + addr_from_index(StakingPrecompile::::INDEX), + encode_with_selector( + selector_u32("getStake(bytes32,bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + H256::from_slice(caller_account.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + substrate_to_evm(stake_after), + ); + }); + } + + #[test] + fn staking_precompile_v2_add_stake_and_reads_match_runtime_state() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1002); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + let total_coldkey_stake = total_coldkey_stake_on_subnet(&caller_account, netuid); + + assert!(stake_after > stake_before); + assert!(total_coldkey_stake >= stake_after); + + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getStake(bytes32,bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + H256::from_slice(caller_account.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + U256::from(stake_after), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getTotalColdkeyStakeOnSubnet(bytes32,uint256)"), + ( + H256::from_slice(caller_account.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + U256::from(total_coldkey_stake), + ); + }); + } + + #[test] + fn staking_precompile_v1_rejects_missing_subnet() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x1003); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + fund_account( + &StakingPrecompile::::account_id(), + INITIAL_STAKE_RAO, + ); + + let rejected = execute_precompile( + &precompiles::>(), + addr_from_index(StakingPrecompile::::INDEX), + caller, + encode_with_selector( + selector_u32("addStake(bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(INVALID_NETUID_U16), + ), + ), + substrate_to_evm(INITIAL_STAKE_RAO), + ) + .expect("staking v1 add stake should route to the precompile"); + + assert!(rejected.is_err()); + assert_eq!( + stake_for(&hotkey, &caller_account, NetUid::from(INVALID_NETUID_U16)), + 0, + ); + }); + } + + #[test] + fn staking_precompile_v2_rejects_missing_subnet() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x1004); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + + let rejected = execute_precompile( + &precompiles::>(), + addr_from_index(StakingPrecompileV2::::INDEX), + caller, + encode_with_selector( + selector_u32("addStake(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(INITIAL_STAKE_RAO), + U256::from(INVALID_NETUID_U16), + ), + ), + U256::zero(), + ) + .expect("staking v2 add stake should route to the precompile"); + + assert!(rejected.is_err()); + assert_eq!( + stake_for(&hotkey, &caller_account, NetUid::from(INVALID_NETUID_U16)), + 0, + ); + }); + } + + #[test] + fn staking_precompile_v1_remove_stake_reduces_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1005); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + add_stake_v1(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + pallet_subtensor::StakingOperationRateLimiter::::remove(( + hotkey.clone(), + caller_account.clone(), + netuid, + )); + + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompile::::INDEX); + let stake_before = stake_for(&hotkey, &caller_account, netuid); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeStake(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + substrate_to_evm(REMOVE_STAKE_RAO), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert_eq!(stake_after, stake_before - REMOVE_STAKE_RAO); + }); + } + + #[test] + fn staking_precompile_v2_remove_stake_reduces_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1006); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + pallet_subtensor::StakingOperationRateLimiter::::remove(( + hotkey.clone(), + caller_account.clone(), + netuid, + )); + + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + let stake_before = stake_for(&hotkey, &caller_account, netuid); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeStake(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(REMOVE_STAKE_RAO), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert_eq!(stake_after, stake_before - REMOVE_STAKE_RAO); + }); + } + + #[test] + fn staking_precompile_v2_add_stake_limit_increases_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x4001); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(INITIAL_STAKE_RAO), + U256::from(1_000_000_000_000_u64), + true, + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + assert!(stake_for(&hotkey, &caller_account, netuid) > stake_before); + }); + } + + #[test] + fn staking_precompile_v2_remove_stake_limit_decreases_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x4002); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(INITIAL_STAKE_RAO), + U256::from(1_000_000_000_000_u64), + true, + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + pallet_subtensor::StakingOperationRateLimiter::::remove(( + hotkey.clone(), + caller_account.clone(), + netuid, + )); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeStakeLimit(bytes32,uint256,uint256,bool,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(REMOVE_STAKE_RAO), + U256::from(1_000_000_000_u64), + true, + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + assert!(stake_for(&hotkey, &caller_account, netuid) < stake_before); + }); + } + + #[test] + fn staking_precompile_v2_remove_stake_full_limit_clears_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x4003); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(INITIAL_STAKE_RAO), + U256::from(1_000_000_000_000_u64), + true, + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + pallet_subtensor::StakingOperationRateLimiter::::remove(( + hotkey.clone(), + caller_account.clone(), + netuid, + )); + + assert!(stake_for(&hotkey, &caller_account, netuid) > 0); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeStakeFullLimit(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + U256::from(90_000_000_u64), + ), + ), + ) + .execute_returns(()); + + assert_eq!(stake_for(&hotkey, &caller_account, netuid), 0); + }); + } + + #[test] + fn staking_precompile_v2_remove_stake_full_clears_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x4004); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(INITIAL_STAKE_RAO), + U256::from(1_000_000_000_000_u64), + true, + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + pallet_subtensor::StakingOperationRateLimiter::::remove(( + hotkey.clone(), + caller_account.clone(), + netuid, + )); + + assert!(stake_for(&hotkey, &caller_account, netuid) > 0); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeStakeFull(bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + assert_eq!(stake_for(&hotkey, &caller_account, netuid), 0); + }); + } + + #[test] + fn staking_precompile_v2_getters_match_runtime_state() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x4005); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + + let stake = stake_for(&hotkey, &caller_account, netuid); + assert!(stake > 0); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getStake(bytes32,bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + H256::from_slice(caller_account.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + U256::from(stake), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getTotalAlphaStaked(bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + U256::from( + pallet_subtensor::Pallet::::get_stake_for_hotkey_on_subnet( + &hotkey, netuid, + ) + .to_u64(), + ), + ); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAlphaStakedValidators(bytes32,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(encode_return_value(vec![H256::from_slice( + caller_account.as_ref(), + )])); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector(selector_u32("getNominatorMinRequiredStake()"), ()), + U256::from(pallet_subtensor::Pallet::::get_nominator_min_required_stake()), + ); + }); + } + + #[test] + fn staking_precompile_v1_adds_and_removes_proxy() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1007); + let caller_account = mapped_account(caller); + let delegate = delegate(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompile::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + fund_account(&delegate, COLDKEY_BALANCE); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("addProxy(bytes32)"), + (H256::from_slice(delegate.as_ref()),), + ), + ) + .execute_returns(()); + assert_proxy_effects(caller, netuid); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeProxy(bytes32)"), + (H256::from_slice(delegate.as_ref()),), + ), + ) + .execute_returns(()); + + let proxies = pallet_subtensor_proxy::Proxies::::get(&caller_account).0; + assert!(proxies.is_empty()); + }); + } + + #[test] + fn staking_precompile_v2_adds_and_removes_proxy() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x1008); + let caller_account = mapped_account(caller); + let delegate = delegate(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + fund_account(&delegate, COLDKEY_BALANCE); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("addProxy(bytes32)"), + (H256::from_slice(delegate.as_ref()),), + ), + ) + .execute_returns(()); + assert_proxy_effects(caller, netuid); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("removeProxy(bytes32)"), + (H256::from_slice(delegate.as_ref()),), + ), + ) + .execute_returns(()); + + let proxies = pallet_subtensor_proxy::Proxies::::get(&caller_account).0; + assert!(proxies.is_empty()); + }); + } + + #[test] + fn staking_precompile_v2_transfer_stake_from_requires_allowance() { + new_test_ext().execute_with(|| { + let (_, source, spender, _, _, hotkey) = setup_approval_state(); + precompiles::>() + .prepare_test( + spender, + addr_from_index(StakingPrecompileV2::::INDEX), + encode_with_selector( + selector_u32( + "transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)", + ), + ( + precompile_utils::solidity::codec::Address(source), + precompile_utils::solidity::codec::Address(spender), + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + U256::from(TEST_NETUID_U16), + U256::from(1_u64), + ), + ), + ) + .execute_reverts(|output| output == b"trying to spend more than allowed"); + }); + } + + #[test] + fn staking_precompile_v2_transfer_stake_from_consumes_allowance_and_moves_stake() { + new_test_ext().execute_with(|| { + let (netuid, source, spender, source_account, spender_account, hotkey) = + setup_approval_state(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + precompiles + .prepare_test( + source, + precompile_addr, + encode_with_selector( + selector_u32("approve(address,uint256,uint256)"), + ( + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + U256::from(APPROVED_ALLOWANCE_RAO), + ), + ), + ) + .execute_returns(()); + + let source_stake_before = stake_for(&hotkey, &source_account, netuid); + let spender_stake_before = stake_for(&hotkey, &spender_account, netuid); + + precompiles + .prepare_test( + spender, + precompile_addr, + encode_with_selector( + selector_u32( + "transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)", + ), + ( + precompile_utils::solidity::codec::Address(source), + precompile_utils::solidity::codec::Address(spender), + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + U256::from(TEST_NETUID_U16), + U256::from(TRANSFERRED_ALLOWANCE_RAO), + ), + ), + ) + .execute_returns(()); + + assert_allowance( + source, + spender, + source, + U256::from(APPROVED_ALLOWANCE_RAO - TRANSFERRED_ALLOWANCE_RAO), + ); + assert_eq!( + stake_for(&hotkey, &source_account, netuid), + source_stake_before - TRANSFERRED_ALLOWANCE_RAO, + ); + assert_eq!( + stake_for(&hotkey, &spender_account, netuid), + spender_stake_before + TRANSFERRED_ALLOWANCE_RAO, + ); + }); + } + + #[test] + fn staking_precompile_v2_transfer_stake_from_rejects_amount_above_allowance() { + new_test_ext().execute_with(|| { + let (_, source, spender, _, _, hotkey) = setup_approval_state(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + precompiles + .prepare_test( + source, + precompile_addr, + encode_with_selector( + selector_u32("approve(address,uint256,uint256)"), + ( + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + U256::from(TRANSFERRED_ALLOWANCE_RAO), + ), + ), + ) + .execute_returns(()); + + precompiles + .prepare_test( + spender, + precompile_addr, + encode_with_selector( + selector_u32( + "transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)", + ), + ( + precompile_utils::solidity::codec::Address(source), + precompile_utils::solidity::codec::Address(spender), + H256::from_slice(hotkey.as_ref()), + U256::from(TEST_NETUID_U16), + U256::from(TEST_NETUID_U16), + U256::from(TRANSFERRED_ALLOWANCE_RAO + 1), + ), + ), + ) + .execute_reverts(|output| output == b"trying to spend more than allowed"); + }); + } + + #[test] + fn staking_precompile_v2_approval_functions_update_allowance() { + new_test_ext().execute_with(|| { + let (_, source, spender, _, _, _) = setup_approval_state(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + assert_allowance(source, spender, source, U256::zero()); + + precompiles + .prepare_test( + source, + precompile_addr, + encode_with_selector( + selector_u32("approve(address,uint256,uint256)"), + ( + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + U256::from(APPROVED_ALLOWANCE_RAO), + ), + ), + ) + .execute_returns(()); + assert_allowance(source, spender, source, U256::from(APPROVED_ALLOWANCE_RAO)); + + precompiles + .prepare_test( + source, + precompile_addr, + encode_with_selector( + selector_u32("increaseAllowance(address,uint256,uint256)"), + ( + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + U256::from(APPROVED_ALLOWANCE_RAO), + ), + ), + ) + .execute_returns(()); + assert_allowance( + source, + spender, + source, + U256::from(APPROVED_ALLOWANCE_RAO * 2), + ); + + precompiles + .prepare_test( + source, + precompile_addr, + encode_with_selector( + selector_u32("decreaseAllowance(address,uint256,uint256)"), + ( + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + U256::from(ALLOWANCE_DECREASE_RAO), + ), + ), + ) + .execute_returns(()); + assert_allowance( + source, + spender, + source, + U256::from(APPROVED_ALLOWANCE_RAO * 2 - ALLOWANCE_DECREASE_RAO), + ); + + precompiles + .prepare_test( + source, + precompile_addr, + encode_with_selector( + selector_u32("approve(address,uint256,uint256)"), + ( + precompile_utils::solidity::codec::Address(spender), + U256::from(TEST_NETUID_U16), + U256::zero(), + ), + ), + ) + .execute_returns(()); + assert_allowance(source, spender, source, U256::zero()); + }); + } + + #[test] + fn staking_precompile_v2_burn_alpha_reduces_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x3001); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let burn_amount = 20_000_000_000_u64; + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + add_stake_v2(caller, &hotkey, TEST_NETUID_U16, 50_000_000_000); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + assert!(stake_before > 0); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("burnAlpha(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(burn_amount), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert_eq!(stake_after, stake_before - burn_amount); + }); + } + + #[test] + fn staking_precompile_v2_burn_alpha_caps_to_available_stake() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x3002); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + assert!(stake_before > 0); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("burnAlpha(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(stake_before + 10_000_000_000_u64), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert_eq!(stake_after, 0); + }); + } + + #[test] + fn staking_precompile_v2_burn_alpha_rejects_missing_subnet() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x3003); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + + fund_account(&caller_account, COLDKEY_BALANCE); + ensure_hotkey_exists(&hotkey); + + let rejected = execute_precompile( + &precompiles::>(), + addr_from_index(StakingPrecompileV2::::INDEX), + caller, + encode_with_selector( + selector_u32("burnAlpha(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::from(10_000_000_000_u64), + U256::from(INVALID_NETUID_U16), + ), + ), + U256::zero(), + ) + .expect("burnAlpha should route to the staking v2 precompile"); + + assert!(rejected.is_err()); + }); + } + + #[test] + fn staking_precompile_v2_burn_zero_alpha_is_noop() { + new_test_ext().execute_with(|| { + let netuid = setup_staking_subnet(); + let caller = addr_from_index(0x3004); + let caller_account = mapped_account(caller); + let hotkey = hotkey(); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); + + fund_account(&caller_account, COLDKEY_BALANCE); + add_stake_v2(caller, &hotkey, TEST_NETUID_U16, 10_000_000_000); + + let stake_before = stake_for(&hotkey, &caller_account, netuid); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("burnAlpha(bytes32,uint256,uint256)"), + ( + H256::from_slice(hotkey.as_ref()), + U256::zero(), + U256::from(TEST_NETUID_U16), + ), + ), + ) + .execute_returns(()); + + let stake_after = stake_for(&hotkey, &caller_account, netuid); + assert_eq!(stake_after, stake_before); + }); + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7c4112837d..0b8e3c982d 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -272,7 +272,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: 401, + spec_version: 402, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,