From 204908ca6a0c9b36e131db4c958415e4c5c6670e Mon Sep 17 00:00:00 2001 From: open-junius Date: Thu, 4 Jun 2026 21:04:00 +0800 Subject: [PATCH 01/10] migrate contract e2e --- ts-tests/moonwall.config.json | 32 ++++++++ ts-tests/pnpm-workspace.yaml | 13 +++ .../00-evm-substrate-transfer.test.ts | 79 +++++++++++++++++++ ts-tests/utils/address.ts | 30 +++++++ ts-tests/utils/balance.ts | 5 ++ ts-tests/utils/index.ts | 13 +-- 6 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts create mode 100644 ts-tests/utils/address.ts diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index cc5667af9f..0b37cf37af 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -120,6 +120,38 @@ "endpoints": ["ws://127.0.0.1:9947"] } ] + }, { + "name": "zombienet_evm", + "timeout": 600000, + "testFileDir": ["suites/zombienet_evm"], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_node.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"], + "descriptor": "subtensor" + }, + { + "name": "EVM", + "type": "ethers", + "endpoints": ["http://127.0.0.1:9947"], + "descriptor": "evm" + } + ] }, { "name": "zombienet_subnets", "timeout": 600000, diff --git a/ts-tests/pnpm-workspace.yaml b/ts-tests/pnpm-workspace.yaml index 856299a3ed..85e8725741 100644 --- a/ts-tests/pnpm-workspace.yaml +++ b/ts-tests/pnpm-workspace.yaml @@ -13,3 +13,16 @@ onlyBuiltDependencies: - protobufjs - sqlite3 - ssh2 + +# Allow exotic subdependencies (needed for toml dependency) +allowExoticSubdeps: true + +allowBuilds: + '@biomejs/biome': set this to true or false + '@parcel/watcher': set this to true or false + cpu-features: set this to true or false + esbuild: set this to true or false + msgpackr-extract: set this to true or false + protobufjs: set this to true or false + sqlite3: set this to true or false + ssh2: set this to true or false diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts new file mode 100644 index 0000000000..bf3ce87726 --- /dev/null +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -0,0 +1,79 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { subtensor } from "@polkadot-api/descriptors"; +import { ethers } from "ethers"; +import type { TypedApi } from "polkadot-api"; +import { convertH160ToSS58, forceSetBalance, raoToEth, tao, waitForFinalizedBlocks } from "../../utils"; + +function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wallet { + const account = ethers.Wallet.createRandom(); + return new ethers.Wallet(account.privateKey, provider); +} + +async function estimateTransactionCost( + provider: ethers.Provider, + tx: ethers.TransactionRequest, +): Promise { + const feeData = await provider.getFeeData(); + const estimatedGas = await provider.estimateGas(tx); + const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas; + if (gasPrice == null) { + return estimatedGas; + } + return estimatedGas * gasPrice; +} + +describeSuite({ + id: "evm-substrate-transfer-basic", + title: "Basic EVM-Substrate Transfer Tests", + foundationMethods: "zombie", + testCases: ({ it, context }) => { + let api: TypedApi; + let ethWallet: ethers.Wallet; + let ethWallet2: ethers.Wallet; + + beforeAll(async () => { + api = context.papi("Node").getTypedApi(subtensor); + + const provider = context.ethers("EVM").provider as ethers.JsonRpcProvider; + ethWallet = createEthersWallet(provider); + ethWallet2 = createEthersWallet(provider); + + await forceSetBalance(api, convertH160ToSS58(ethWallet.address)); + await forceSetBalance(api, convertH160ToSS58(ethWallet2.address)); + await waitForFinalizedBlocks(api, 1); + }, 120000); + + it({ + id: "T01", + title: "Can transfer token from EVM to EVM", + test: async () => { + const provider = ethWallet.provider; + if (provider == null) { + throw new Error("ethWallet has no provider"); + } + + const senderBalanceBefore = await provider.getBalance(ethWallet.address); + const receiverBalanceBefore = await provider.getBalance(ethWallet2.address); + + const transferAmount = raoToEth(tao(1)); + const tx: ethers.TransactionRequest = { + to: ethWallet2.address, + value: transferAmount, + }; + + const txFee = await estimateTransactionCost(provider, tx); + + const txResponse = await ethWallet.sendTransaction(tx); + const receipt = await txResponse.wait(); + expect(receipt).toBeDefined(); + expect(receipt!.status).toEqual(1); + + const senderBalanceAfter = await provider.getBalance(ethWallet.address); + const receiverBalanceAfter = await provider.getBalance(ethWallet2.address); + + expect(senderBalanceAfter).toEqual(senderBalanceBefore - transferAmount - txFee); + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + transferAmount); + }, + }); + }, +}); diff --git a/ts-tests/utils/address.ts b/ts-tests/utils/address.ts new file mode 100644 index 0000000000..8e7909c20f --- /dev/null +++ b/ts-tests/utils/address.ts @@ -0,0 +1,30 @@ +import { hexToU8a } from "@polkadot/util"; +import { blake2AsU8a, encodeAddress } from "@polkadot/util-crypto"; + +const SS58_PREFIX = 42; + +export function convertH160ToPublicKey(ethAddress: string) { + const prefix = "evm:"; + const prefixBytes = new TextEncoder().encode(prefix); + const addressBytes = hexToU8a( + ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}` + ); + const combined = new Uint8Array(prefixBytes.length + addressBytes.length); + + // Concatenate prefix and Ethereum address + combined.set(prefixBytes); + combined.set(addressBytes, prefixBytes.length); + + // Hash the combined data (the public key) + const hash = blake2AsU8a(combined); + return hash; +} + +export function convertH160ToSS58(ethAddress: string) { + // get the public key + const hash = convertH160ToPublicKey(ethAddress); + + // Convert the hash to SS58 format + const ss58Address = encodeAddress(hash, SS58_PREFIX); + return ss58Address; +} \ No newline at end of file diff --git a/ts-tests/utils/balance.ts b/ts-tests/utils/balance.ts index f6fe83d3b0..9794f2d9cb 100644 --- a/ts-tests/utils/balance.ts +++ b/ts-tests/utils/balance.ts @@ -9,6 +9,11 @@ export function tao(value: number): bigint { return TAO * BigInt(value); } +/** Convert RAO to the EVM native balance unit (1 RAO → 1 gwei on-chain). */ +export function raoToEth(rao: bigint): bigint { + return TAO * rao; +} + export async function getBalance(api: TypedApi, ss58Address: string): Promise { const account = await api.query.System.Account.getValue(ss58Address); return account.data.free; diff --git a/ts-tests/utils/index.ts b/ts-tests/utils/index.ts index b3aa36d528..499d4ea469 100644 --- a/ts-tests/utils/index.ts +++ b/ts-tests/utils/index.ts @@ -1,7 +1,10 @@ -export * from "./transactions.js"; -export * from "./balance.js"; -export * from "./subnet.js"; -export * from "./staking.js"; -export * from "./shield_helpers.ts"; export * from "./account.ts"; +export * from "./address.ts"; +export * from "./balance.js"; export * from "./coldkey_swap.ts"; +export * from "./config.js"; +export * from "./shield_helpers.ts"; +export * from "./staking.js"; +export * from "./subnet.js"; +export * from "./transactions.js"; + From 8959e8f5aaa989438ec5ed57af3d22fe0f125665 Mon Sep 17 00:00:00 2001 From: "subtensor-ai-review[bot]" Date: Thu, 4 Jun 2026 13:13:15 +0000 Subject: [PATCH 02/10] chore: auditor auto-fix --- ts-tests/utils/address.ts | 2 +- ts-tests/utils/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ts-tests/utils/address.ts b/ts-tests/utils/address.ts index 8e7909c20f..858cf06833 100644 --- a/ts-tests/utils/address.ts +++ b/ts-tests/utils/address.ts @@ -27,4 +27,4 @@ export function convertH160ToSS58(ethAddress: string) { // Convert the hash to SS58 format const ss58Address = encodeAddress(hash, SS58_PREFIX); return ss58Address; -} \ No newline at end of file +} diff --git a/ts-tests/utils/index.ts b/ts-tests/utils/index.ts index 499d4ea469..9e5c56527a 100644 --- a/ts-tests/utils/index.ts +++ b/ts-tests/utils/index.ts @@ -7,4 +7,3 @@ export * from "./shield_helpers.ts"; export * from "./staking.js"; export * from "./subnet.js"; export * from "./transactions.js"; - From faaf2732d5c15a9f9553f2df7d6aada6f21b1927 Mon Sep 17 00:00:00 2001 From: open-junius Date: Thu, 4 Jun 2026 22:36:18 +0800 Subject: [PATCH 03/10] update ci configure --- .github/workflows/typescript-e2e.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 82c63e1356..d2b1182640 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -107,6 +107,8 @@ jobs: binary: fast - test: zombienet_subnets binary: fast + - test: zombienet_evm + binary: fast name: "typescript-e2e-${{ matrix.test }}" From 5ed2219fc86cbce5a208d037991318147208ae6e Mon Sep 17 00:00:00 2001 From: open-junius Date: Thu, 4 Jun 2026 23:43:50 +0800 Subject: [PATCH 04/10] fmt file --- ts-tests/utils/address.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts-tests/utils/address.ts b/ts-tests/utils/address.ts index 858cf06833..fb9c171c33 100644 --- a/ts-tests/utils/address.ts +++ b/ts-tests/utils/address.ts @@ -7,7 +7,7 @@ export function convertH160ToPublicKey(ethAddress: string) { const prefix = "evm:"; const prefixBytes = new TextEncoder().encode(prefix); const addressBytes = hexToU8a( - ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}` + ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}`, ); const combined = new Uint8Array(prefixBytes.length + addressBytes.length); From 62e1210707a24cafa53af45224c18242b94d1977 Mon Sep 17 00:00:00 2001 From: open-junius Date: Fri, 5 Jun 2026 09:29:21 +0800 Subject: [PATCH 05/10] fix format --- .../suites/zombienet_evm/00-evm-substrate-transfer.test.ts | 5 +---- ts-tests/utils/address.ts | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index bf3ce87726..ffb92bcbf9 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -9,10 +9,7 @@ function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wallet { return new ethers.Wallet(account.privateKey, provider); } -async function estimateTransactionCost( - provider: ethers.Provider, - tx: ethers.TransactionRequest, -): Promise { +async function estimateTransactionCost(provider: ethers.Provider, tx: ethers.TransactionRequest): Promise { const feeData = await provider.getFeeData(); const estimatedGas = await provider.estimateGas(tx); const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas; diff --git a/ts-tests/utils/address.ts b/ts-tests/utils/address.ts index fb9c171c33..fa603e675e 100644 --- a/ts-tests/utils/address.ts +++ b/ts-tests/utils/address.ts @@ -6,9 +6,7 @@ const SS58_PREFIX = 42; export function convertH160ToPublicKey(ethAddress: string) { const prefix = "evm:"; const prefixBytes = new TextEncoder().encode(prefix); - const addressBytes = hexToU8a( - ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}`, - ); + const addressBytes = hexToU8a(ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}`); const combined = new Uint8Array(prefixBytes.length + addressBytes.length); // Concatenate prefix and Ethereum address From 5ffa34474d56e2a0db50a6069b0287cf8b01a9b2 Mon Sep 17 00:00:00 2001 From: open-junius Date: Fri, 5 Jun 2026 10:08:28 +0800 Subject: [PATCH 06/10] commit Cargo.lock --- pallets/subtensor/src/staking/order_swap.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pallets/subtensor/src/staking/order_swap.rs b/pallets/subtensor/src/staking/order_swap.rs index 98643caae6..bac78742eb 100644 --- a/pallets/subtensor/src/staking/order_swap.rs +++ b/pallets/subtensor/src/staking/order_swap.rs @@ -47,10 +47,7 @@ impl OrderSwapInterface for Pallet { ); } let alpha_out = - Self::stake_into_subnet(hotkey, coldkey, netuid, tao_amount, amm_limit, false, false)?; - if validate { - Self::set_stake_operation_limit(hotkey, coldkey, netuid); - } + Self::stake_into_subnet(hotkey, coldkey, netuid, tao_amount, amm_limit, false)?; Ok(alpha_out) } @@ -136,7 +133,6 @@ impl OrderSwapInterface for Pallet { TaoBalance::from(tao_equiv) >= DefaultMinStake::::get(), Error::::AmountTooLow ); - Self::ensure_stake_operation_limit_not_exceeded(from_hotkey, from_coldkey, netuid)?; Self::ensure_available_to_unstake(from_coldkey, netuid, amount)?; } @@ -145,7 +141,6 @@ impl OrderSwapInterface for Pallet { Self::hotkey_account_exists(to_hotkey), Error::::HotKeyAccountNotExists ); - Self::set_stake_operation_limit(to_hotkey, to_coldkey, netuid); } let available = From b1718a84f72bda5c9cea89fd0c7fd73b0dd819f2 Mon Sep 17 00:00:00 2001 From: open-junius Date: Fri, 5 Jun 2026 10:39:10 +0800 Subject: [PATCH 07/10] remove config import --- ts-tests/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts-tests/utils/index.ts b/ts-tests/utils/index.ts index 9e5c56527a..3a91d860a0 100644 --- a/ts-tests/utils/index.ts +++ b/ts-tests/utils/index.ts @@ -2,8 +2,8 @@ export * from "./account.ts"; export * from "./address.ts"; export * from "./balance.js"; export * from "./coldkey_swap.ts"; -export * from "./config.js"; export * from "./shield_helpers.ts"; export * from "./staking.js"; export * from "./subnet.js"; export * from "./transactions.js"; + From 86ffee76f706cbbbcf5f5d3635d9f21447e8de76 Mon Sep 17 00:00:00 2001 From: open-junius Date: Fri, 5 Jun 2026 21:21:12 +0800 Subject: [PATCH 08/10] migrate whole evm substrate transfer test --- .../test/eth.substrate-transfer.test.ts | 407 --------------- .../00-evm-substrate-transfer.test.ts | 486 +++++++++++++++++- ts-tests/utils/address.ts | 42 +- ts-tests/utils/balance.ts | 15 +- ts-tests/utils/evm-config.ts | 46 ++ ts-tests/utils/evm.ts | 29 ++ ts-tests/utils/index.ts | 2 + ts-tests/utils/transactions.ts | 6 +- 8 files changed, 587 insertions(+), 446 deletions(-) delete mode 100644 contract-tests/test/eth.substrate-transfer.test.ts create mode 100644 ts-tests/utils/evm-config.ts create mode 100644 ts-tests/utils/evm.ts diff --git a/contract-tests/test/eth.substrate-transfer.test.ts b/contract-tests/test/eth.substrate-transfer.test.ts deleted file mode 100644 index fc8073585c..0000000000 --- a/contract-tests/test/eth.substrate-transfer.test.ts +++ /dev/null @@ -1,407 +0,0 @@ -import * as assert from "assert"; - -import { getDevnetApi, waitForTransactionCompletion, getRandomSubstrateSigner, waitForTransactionWithRetry } from "../src/substrate" -import { getPublicClient } from "../src/utils"; -import { ETH_LOCAL_URL, IBALANCETRANSFER_ADDRESS, IBalanceTransferABI } from "../src/config"; -import { devnet, MultiAddress } from "@polkadot-api/descriptors" -import { PublicClient } from "viem"; -import { TypedApi, Binary, FixedSizeBinary } from "polkadot-api"; -import { generateRandomEthersWallet } from "../src/utils"; -import { tao, raoToEth, bigintToRao, compareEthBalanceWithTxFee } from "../src/balance-math"; -import { toViemAddress, convertPublicKeyToSs58, convertH160ToSS58, ss58ToH160, ss58ToEthAddress, ethAddressToH160 } from "../src/address-utils" -import { ethers } from "ethers" -import { estimateTransactionCost, getContract } from "../src/eth" - -import { WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE } from "../src/contracts/withdraw" - -import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, disableWhiteListCheck } from "../src/subtensor"; - -describe("Balance transfers between substrate and EVM", () => { - const gwei = BigInt("1000000000"); - // init eth part - const wallet = generateRandomEthersWallet(); - const wallet2 = generateRandomEthersWallet(); - let publicClient: PublicClient; - const provider = new ethers.JsonRpcProvider(ETH_LOCAL_URL); - // init substrate part - const signer = getRandomSubstrateSigner(); - let api: TypedApi - - before(async () => { - - publicClient = await getPublicClient(ETH_LOCAL_URL) - api = await getDevnetApi() - - await forceSetBalanceToEthAddress(api, wallet.address) - await forceSetBalanceToEthAddress(api, wallet2.address) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(signer.publicKey)) - await disableWhiteListCheck(api, true) - }); - - it("Can transfer token from EVM to EVM", async () => { - const senderBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - const transferBalance = raoToEth(tao(1)) - const tx = { - to: wallet2.address, - value: transferBalance.toString() - } - const txFee = await estimateTransactionCost(provider, tx) - - const txResponse = await wallet.sendTransaction(tx) - await txResponse.wait(); - - - const senderBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalanceAfterTranser = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - - assert.equal(senderBalanceAfterTransfer, senderBalance - transferBalance - txFee) - assert.equal(receiverBalance, receiverBalanceAfterTranser - transferBalance) - }); - - it("Can transfer token from Substrate to EVM", async () => { - const ss58Address = convertH160ToSS58(wallet.address) - const senderBalance = (await api.query.System.Account.getValue(ss58Address)).data.free - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const transferBalance = tao(1) - - const tx = api.tx.Balances.transfer_keep_alive({ value: transferBalance, dest: MultiAddress.Id(ss58Address) }) - await waitForTransactionWithRetry(api, tx, signer) - - const senderBalanceAfterTransfer = (await api.query.System.Account.getValue(ss58Address)).data.free - const receiverBalanceAfterTranser = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - - assert.equal(senderBalanceAfterTransfer, senderBalance + transferBalance) - assert.equal(receiverBalance, receiverBalanceAfterTranser - raoToEth(transferBalance)) - }); - - it("Can transfer token from EVM to Substrate", async () => { - const contract = getContract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, wallet) - const senderBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalance = (await api.query.System.Account.getValue(convertPublicKeyToSs58(signer.publicKey))).data.free - const transferBalance = raoToEth(tao(1)) - - const tx = await contract.transfer(signer.publicKey, { value: transferBalance.toString() }) - await tx.wait() - - - const senderBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalanceAfterTranser = (await api.query.System.Account.getValue(convertPublicKeyToSs58(signer.publicKey))).data.free - - compareEthBalanceWithTxFee(senderBalanceAfterTransfer, senderBalance - transferBalance) - assert.equal(receiverBalance, receiverBalanceAfterTranser - tao(1)) - }); - - it("Transfer from EVM to substrate using evm::withdraw", async () => { - const ss58Address = convertPublicKeyToSs58(signer.publicKey) - const senderBalance = (await api.query.System.Account.getValue(ss58Address)).data.free - const ethAddresss = ss58ToH160(ss58Address); - - // transfer token to mirror eth address - const ethTransfer = { - to: ss58ToEthAddress(ss58Address), - value: raoToEth(tao(2)).toString() - } - - const txResponse = await wallet.sendTransaction(ethTransfer) - await txResponse.wait(); - - const tx = api.tx.EVM.withdraw({ address: ethAddresss, value: tao(1) }) - const txFee = (await tx.getPaymentInfo(ss58Address)).partial_fee - - await waitForTransactionWithRetry(api, tx, signer) - - const senderBalanceAfterWithdraw = (await api.query.System.Account.getValue(ss58Address)).data.free - - assert.equal(senderBalance, senderBalanceAfterWithdraw - tao(1) + txFee) - }); - - it("Transfer from EVM to substrate using evm::call", async () => { - const ss58Address = convertPublicKeyToSs58(signer.publicKey) - const ethAddresss = ss58ToH160(ss58Address); - - // transfer token to mirror eth address - const ethTransfer = { - to: ss58ToEthAddress(ss58Address), - value: raoToEth(tao(2)).toString() - } - - const txResponse = await wallet.sendTransaction(ethTransfer) - await txResponse.wait(); - - const source: FixedSizeBinary<20> = ethAddresss; - const target = ethAddressToH160(wallet.address) - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - - // all these parameter value are tricky, any change could make the call failed - const tx = api.tx.EVM.call({ - source: source, - target: target, - // it is U256 in the extrinsic. - value: [raoToEth(tao(1)), tao(0), tao(0), tao(0)], - gas_limit: BigInt(1000000), - // it is U256 in the extrinsic. - max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], - max_priority_fee_per_gas: undefined, - input: Binary.fromText(""), - nonce: undefined, - access_list: [], - authorization_list: [] - }) - // txFee not accurate - const txFee = (await tx.getPaymentInfo(ss58Address)).partial_fee - - await waitForTransactionWithRetry(api, tx, signer) - - const receiverBalanceAfterCall = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - assert.equal(receiverBalanceAfterCall, receiverBalance + raoToEth(tao(1))) - }); - - it("Forward value in smart contract", async () => { - - - const contractFactory = new ethers.ContractFactory(WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE, wallet) - const contract = await contractFactory.deploy() - await contract.waitForDeployment() - - const code = await publicClient.getCode({ address: toViemAddress(contract.target.toString()) }) - if (code === undefined) { - throw new Error("code length is wrong for deployed contract") - } - assert.ok(code.length > 100) - - // transfer 2 TAO to contract - const ethTransfer = { - to: contract.target.toString(), - value: raoToEth(tao(2)).toString() - } - - const txResponse = await wallet.sendTransaction(ethTransfer) - await txResponse.wait(); - - const contractBalance = await publicClient.getBalance({ address: toViemAddress(contract.target.toString()) }) - const callerBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - - const contractForCall = new ethers.Contract(contract.target.toString(), WITHDRAW_CONTRACT_ABI, wallet) - - const withdrawTx = await contractForCall.withdraw( - raoToEth(tao(1)).toString() - ); - - await withdrawTx.wait(); - - const contractBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(contract.target.toString()) }) - const callerBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - - compareEthBalanceWithTxFee(callerBalanceAfterWithdraw, callerBalance + raoToEth(tao(1))) - assert.equal(contractBalance, contractBalanceAfterWithdraw + raoToEth(tao(1))) - }); - - it("Transfer full balance", async () => { - const ethBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - const tx = { - to: wallet2.address, - value: ethBalance.toString(), - }; - const txPrice = await estimateTransactionCost(provider, tx); - const finalTx = { - to: wallet2.address, - value: (ethBalance - txPrice).toString(), - }; - try { - // transfer should be failed since substrate requires existial balance to keep account - const txResponse = await wallet.sendTransaction(finalTx) - await txResponse.wait(); - } catch (error) { - if (error instanceof Error) { - assert.equal((error as any).code, "INSUFFICIENT_FUNDS") - assert.equal(error.toString().includes("insufficient funds"), true) - } - } - - const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - assert.equal(receiverBalance, receiverBalanceAfterTransfer) - }) - - it("Transfer more than owned balance should fail", async () => { - const ethBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - const tx = { - to: wallet2.address, - value: (ethBalance + raoToEth(tao(1))).toString(), - }; - - try { - // transfer should be failed since substrate requires existial balance to keep account - const txResponse = await wallet.sendTransaction(tx) - await txResponse.wait(); - } catch (error) { - if (error instanceof Error) { - assert.equal((error as any).code, "INSUFFICIENT_FUNDS") - assert.equal(error.toString().includes("insufficient funds"), true) - } - } - - const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - assert.equal(receiverBalance, receiverBalanceAfterTransfer) - }); - - it("Transfer more than u64::max in substrate equivalent should receive error response", async () => { - const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - try { - const tx = { - to: wallet2.address, - value: raoToEth(BigInt(2) ** BigInt(64)).toString(), - }; - // transfer should be failed since substrate requires existial balance to keep account - const txResponse = await wallet.sendTransaction(tx) - await txResponse.wait(); - } catch (error) { - if (error instanceof Error) { - assert.equal((error as any).code, "INSUFFICIENT_FUNDS") - assert.equal(error.toString().includes("insufficient funds"), true) - } - } - - const contract = getContract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, wallet) - try { - const tx = await contract.transfer(signer.publicKey, { value: raoToEth(BigInt(2) ** BigInt(64)).toString() }) - await tx.await() - } catch (error) { - if (error instanceof Error) { - console.log(error.toString()) - assert.equal(error.toString().includes("revert data"), true) - } - } - - try { - const dest = convertH160ToSS58(wallet2.address) - const tx = api.tx.Balances.transfer_keep_alive({ value: bigintToRao(BigInt(2) ** BigInt(64)), dest: MultiAddress.Id(dest) }) - await waitForTransactionCompletion(api, tx, signer) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - } catch (error) { - if (error instanceof Error) { - console.log(error.toString()) - assert.equal(error.toString().includes("Cannot convert"), true) - } - } - - try { - const dest = ethAddressToH160(wallet2.address) - const tx = api.tx.EVM.withdraw({ value: bigintToRao(BigInt(2) ** BigInt(64)), address: dest }) - await waitForTransactionCompletion(api, tx, signer) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - } catch (error) { - if (error instanceof Error) { - assert.equal(error.toString().includes("Cannot convert"), true) - } - } - - try { - const source = ethAddressToH160(wallet.address) - const target = ethAddressToH160(wallet2.address) - const tx = api.tx.EVM.call({ - source: source, - target: target, - // it is U256 in the extrinsic, the value is more than u64::MAX - value: [raoToEth(tao(1)), tao(0), tao(0), tao(1)], - gas_limit: BigInt(1000000), - // it is U256 in the extrinsic. - max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], - max_priority_fee_per_gas: undefined, - input: Binary.fromText(""), - nonce: undefined, - access_list: [], - authorization_list: [] - }) - await waitForTransactionCompletion(api, tx, signer) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - } catch (error) { - if (error instanceof Error) { - console.log(error.toString()) - assert.equal((error as any).code, "INSUFFICIENT_FUNDS") - assert.equal(error.toString().includes("insufficient funds"), true) - } - } - - const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) - assert.equal(receiverBalance, receiverBalanceAfterTransfer) - }); - - it("Gas price should be 10 GWei", async () => { - const feeData = await provider.getFeeData(); - assert.equal(feeData.gasPrice, BigInt(10000000000)); - }); - - - it("max_fee_per_gas and max_priority_fee_per_gas affect transaction fee properly", async () => { - - const testCases = [ - [10, 0, 21000 * 10 * 1e9], - [10, 10, 21000 * 10 * 1e9], - [11, 0, 21000 * 10 * 1e9], - // max_priority_fee_per_gas is disabled - // [11, 1, (21000 * 10 + 21000) * 1e9], - // [11, 2, (21000 * 10 + 21000) * 1e9], - ]; - - for (let i in testCases) { - const tc = testCases[i]; - const actualFee = await transferAndGetFee( - wallet, wallet2, publicClient, - gwei * BigInt(tc[0]), - gwei * BigInt(tc[1]) - ); - assert.equal(actualFee, BigInt(tc[2])) - } - }); - - it("Low max_fee_per_gas gets transaction rejected", async () => { - try { - await transferAndGetFee(wallet, wallet2, publicClient, gwei * BigInt(9), BigInt(0)) - } catch (error) { - if (error instanceof Error) { - console.log(error.toString()) - assert.equal(error.toString().includes("gas price less than block base fee"), true) - } - } - }); - - it("max_fee_per_gas lower than max_priority_fee_per_gas gets transaction rejected", async () => { - try { - await transferAndGetFee(wallet, wallet2, publicClient, gwei * BigInt(10), gwei * BigInt(11)) - } catch (error) { - if (error instanceof Error) { - assert.equal(error.toString().includes("priorityFee cannot be more than maxFee"), true) - } - } - }); -}); - -async function transferAndGetFee(wallet: ethers.Wallet, wallet2: ethers.Wallet, client: PublicClient, max_fee_per_gas: BigInt, max_priority_fee_per_gas: BigInt) { - - const ethBalanceBefore = await client.getBalance({ address: toViemAddress(wallet.address) }) - // Send TAO - const tx = { - to: wallet2.address, - value: raoToEth(tao(1)).toString(), - // EIP-1559 transaction parameters - maxPriorityFeePerGas: max_priority_fee_per_gas.toString(), - maxFeePerGas: max_fee_per_gas.toString(), - gasLimit: 21000, - }; - - // Send the transaction - const txResponse = await wallet.sendTransaction(tx); - await txResponse.wait() - - // Check balances - const ethBalanceAfter = await client.getBalance({ address: toViemAddress(wallet.address) }) - const fee = ethBalanceBefore - ethBalanceAfter - raoToEth(tao(1)) - - return fee; -} \ No newline at end of file diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index ffb92bcbf9..36980228e7 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -1,15 +1,39 @@ import { beforeAll, describeSuite, expect } from "@moonwall/cli"; -import { subtensor } from "@polkadot-api/descriptors"; +import { MultiAddress, subtensor } from "@polkadot-api/descriptors"; +import type { KeyringPair } from "@polkadot/keyring/types"; import { ethers } from "ethers"; import type { TypedApi } from "polkadot-api"; -import { convertH160ToSS58, forceSetBalance, raoToEth, tao, waitForFinalizedBlocks } from "../../utils"; +import { Binary } from "polkadot-api"; +import { + bigintToRao, + convertH160ToSS58, + convertPublicKeyToSs58, + createEthersWallet, + disableWhiteListCheck, + ethAddressToH160, + forceSetBalance, + generateKeyringPair, + getBalance, + getEthBalance, + GWEI, + IBALANCETRANSFER_ADDRESS, + IBalanceTransferABI, + MAX_TX_FEE, + raoToEth, + sendTransaction, + ss58ToEthAddress, + ss58ToH160, + tao, + waitForFinalizedBlocks, + waitForTransactionWithRetry, + WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE +} from "../../utils"; -function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wallet { - const account = ethers.Wallet.createRandom(); - return new ethers.Wallet(account.privateKey, provider); -} -async function estimateTransactionCost(provider: ethers.Provider, tx: ethers.TransactionRequest): Promise { +async function estimateTransactionCost( + provider: ethers.Provider, + tx: ethers.TransactionRequest, +): Promise { const feeData = await provider.getFeeData(); const estimatedGas = await provider.estimateGas(tx); const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas; @@ -19,6 +43,35 @@ async function estimateTransactionCost(provider: ethers.Provider, tx: ethers.Tra return estimatedGas * gasPrice; } +function expectWithinTxFee(actual: bigint, expected: bigint): void { + const diff = actual > expected ? actual - expected : expected - actual; + expect(diff).toBeLessThan(MAX_TX_FEE); +} + +async function transferAndGetFee( + wallet: ethers.Wallet, + wallet2: ethers.Wallet, + provider: ethers.Provider, + maxFeePerGas: bigint, + maxPriorityFeePerGas: bigint, +): Promise { + const ethBalanceBefore = await getEthBalance(provider, wallet.address); + const tx = { + to: wallet2.address, + value: raoToEth(tao(1)).toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), + maxFeePerGas: maxFeePerGas.toString(), + gasLimit: 21000, + }; + + const txResponse = await wallet.sendTransaction(tx); + const receipt = await txResponse.wait(); + expect(receipt?.status).toEqual(1); + + const ethBalanceAfter = await getEthBalance(provider, wallet.address); + return ethBalanceBefore - ethBalanceAfter - raoToEth(tao(1)); +} + describeSuite({ id: "evm-substrate-transfer-basic", title: "Basic EVM-Substrate Transfer Tests", @@ -27,30 +80,30 @@ describeSuite({ let api: TypedApi; let ethWallet: ethers.Wallet; let ethWallet2: ethers.Wallet; + let signer: KeyringPair; + let provider: ethers.JsonRpcProvider; beforeAll(async () => { api = context.papi("Node").getTypedApi(subtensor); - const provider = context.ethers("EVM").provider as ethers.JsonRpcProvider; + provider = context.ethers("EVM").provider as ethers.JsonRpcProvider; ethWallet = createEthersWallet(provider); ethWallet2 = createEthersWallet(provider); + signer = generateKeyringPair("sr25519"); + await forceSetBalance(api, convertPublicKeyToSs58(signer.publicKey)); await forceSetBalance(api, convertH160ToSS58(ethWallet.address)); await forceSetBalance(api, convertH160ToSS58(ethWallet2.address)); + await disableWhiteListCheck(api, true); await waitForFinalizedBlocks(api, 1); - }, 120000); + }, 300000); it({ id: "T01", title: "Can transfer token from EVM to EVM", test: async () => { - const provider = ethWallet.provider; - if (provider == null) { - throw new Error("ethWallet has no provider"); - } - - const senderBalanceBefore = await provider.getBalance(ethWallet.address); - const receiverBalanceBefore = await provider.getBalance(ethWallet2.address); + const senderBalanceBefore = await getEthBalance(provider, ethWallet.address); + const receiverBalanceBefore = await getEthBalance(provider, ethWallet2.address); const transferAmount = raoToEth(tao(1)); const tx: ethers.TransactionRequest = { @@ -65,12 +118,409 @@ describeSuite({ expect(receipt).toBeDefined(); expect(receipt!.status).toEqual(1); - const senderBalanceAfter = await provider.getBalance(ethWallet.address); - const receiverBalanceAfter = await provider.getBalance(ethWallet2.address); + const senderBalanceAfter = await getEthBalance(provider, ethWallet.address); + const receiverBalanceAfter = await getEthBalance(provider, ethWallet2.address); expect(senderBalanceAfter).toEqual(senderBalanceBefore - transferAmount - txFee); expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + transferAmount); }, }); + + it({ + id: "T02", + title: "Can transfer token from Substrate to EVM", + test: async () => { + const ss58Address = convertH160ToSS58(ethWallet.address); + const receiverBalance = await getEthBalance(provider, ethWallet.address); + const transferBalance = tao(1); + + const tx = api.tx.Balances.transfer_keep_alive({ + value: transferBalance, + dest: MultiAddress.Id(ss58Address), + }); + await waitForTransactionWithRetry(api, tx, signer, "substrate_to_evm"); + + const receiverBalanceAfter = await getEthBalance(provider, ethWallet.address); + expect(receiverBalanceAfter).toEqual(receiverBalance + raoToEth(transferBalance)); + }, + }); + + it({ + id: "T03", + title: "Can transfer token from EVM to Substrate", + test: async () => { + const contract = new ethers.Contract( + IBALANCETRANSFER_ADDRESS, + IBalanceTransferABI, + ethWallet, + ); + const signerSs58 = convertPublicKeyToSs58(signer.publicKey); + + const senderBalance = await getEthBalance(provider, ethWallet.address); + const receiverBalance = await getBalance(api, signerSs58); + const transferBalance = raoToEth(tao(1)); + + const tx = await contract.transfer(signer.publicKey, { value: transferBalance.toString() }); + const receipt = await tx.wait(); + expect(receipt?.status).toEqual(1); + + await waitForFinalizedBlocks(api, 2); + + const senderBalanceAfter = await getEthBalance(provider, ethWallet.address); + const receiverBalanceAfter = await getBalance(api, signerSs58); + + expectWithinTxFee(senderBalanceAfter, senderBalance - transferBalance); + expect(receiverBalance).toEqual(receiverBalanceAfter - tao(1)); + }, + }); + + it({ + id: "T04", + title: "Transfer from EVM to substrate using evm::withdraw", + test: async () => { + const ss58Address = convertPublicKeyToSs58(signer.publicKey); + const senderBalance = await getBalance(api, ss58Address); + const ethAddress = ss58ToH160(ss58Address); + + const ethTransfer = { + to: ss58ToEthAddress(ss58Address), + value: raoToEth(tao(2)).toString(), + }; + const fundReceipt = await (await ethWallet.sendTransaction(ethTransfer)).wait(); + expect(fundReceipt?.status).toEqual(1); + + const tx = api.tx.EVM.withdraw({ address: ethAddress, value: tao(1) }); + const txFee = (await tx.getPaymentInfo(ss58Address)).partial_fee; + + await waitForTransactionWithRetry(api, tx, signer, "evm_withdraw", 5); + + const senderBalanceAfterWithdraw = await getBalance(api, ss58Address); + expect(senderBalance).toEqual(senderBalanceAfterWithdraw - tao(1) + txFee); + }, + }); + + it({ + id: "T05", + title: "Transfer from EVM to substrate using evm::call", + test: async () => { + const ss58Address = convertPublicKeyToSs58(signer.publicKey); + const ethAddress = ss58ToH160(ss58Address); + + const ethTransfer = { + to: ss58ToEthAddress(ss58Address), + value: raoToEth(tao(2)).toString(), + }; + const fundReceipt = await (await ethWallet.sendTransaction(ethTransfer)).wait(); + expect(fundReceipt?.status).toEqual(1); + + const source = ethAddress; + const target = ethAddressToH160(ethWallet.address); + const receiverBalance = await getEthBalance(provider, ethWallet.address); + + const tx = api.tx.EVM.call({ + source, + target, + value: [raoToEth(tao(1)), tao(0), tao(0), tao(0)], + gas_limit: BigInt(1000000), + max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], + max_priority_fee_per_gas: undefined, + // PAPI encodes this field with the Binary codec despite the Uint8Array annotation. + input: Binary.fromText("") as unknown as Uint8Array, + nonce: undefined, + access_list: [], + authorization_list: [], + }); + + await waitForTransactionWithRetry(api, tx, signer, "evm_call", 5); + + const receiverBalanceAfterCall = await getEthBalance(provider, ethWallet.address); + expect(receiverBalanceAfterCall).toEqual(receiverBalance + raoToEth(tao(1))); + }, + }); + + it({ + id: "T06", + title: "Forward value in smart contract", + test: async () => { + const contractFactory = new ethers.ContractFactory( + WITHDRAW_CONTRACT_ABI, + WITHDRAW_CONTRACT_BYTECODE, + ethWallet, + ); + const contract = await contractFactory.deploy(); + await contract.waitForDeployment(); + + const contractAddress = contract.target.toString(); + const code = await provider.getCode(contractAddress); + expect(code).toBeDefined(); + expect(code.length).toBeGreaterThan(100); + + const ethTransfer = { + to: contractAddress, + value: raoToEth(tao(2)).toString(), + }; + const fundReceipt = await (await ethWallet.sendTransaction(ethTransfer)).wait(); + expect(fundReceipt?.status).toEqual(1); + + const contractBalance = await getEthBalance(provider, contractAddress); + const callerBalance = await getEthBalance(provider, ethWallet.address); + + const contractForCall = new ethers.Contract(contractAddress, WITHDRAW_CONTRACT_ABI, ethWallet); + const withdrawTx = await contractForCall.withdraw(raoToEth(tao(1)).toString()); + const withdrawReceipt = await withdrawTx.wait(); + expect(withdrawReceipt?.status).toEqual(1); + + const contractBalanceAfterWithdraw = await getEthBalance(provider, contractAddress); + const callerBalanceAfterWithdraw = await getEthBalance(provider, ethWallet.address); + + expectWithinTxFee(callerBalanceAfterWithdraw, callerBalance + raoToEth(tao(1))); + expect(contractBalance).toEqual(contractBalanceAfterWithdraw + raoToEth(tao(1))); + }, + }); + + it({ + id: "T07", + title: "Transfer full balance", + test: async () => { + const ethBalance = await getEthBalance(provider, ethWallet.address); + const receiverBalance = await getEthBalance(provider, ethWallet2.address); + const txPrice = await estimateTransactionCost(provider, { + to: ethWallet2.address, + value: ethBalance.toString(), + }); + const finalTx = { + to: ethWallet2.address, + value: (ethBalance - txPrice).toString(), + }; + + let rejected = false; + try { + const txResponse = await ethWallet.sendTransaction(finalTx); + await txResponse.wait(); + } catch (error) { + rejected = true; + if (error instanceof Error) { + expect( + (error as { code?: string }).code === "INSUFFICIENT_FUNDS" || + error.message.includes("insufficient funds"), + ).toBe(true); + } + } + expect(rejected).toBe(true); + + const receiverBalanceAfterTransfer = await getEthBalance(provider, ethWallet2.address); + expect(receiverBalanceAfterTransfer).toEqual(receiverBalance); + }, + }); + + it({ + id: "T08", + title: "Transfer more than owned balance should fail", + test: async () => { + const ethBalance = await getEthBalance(provider, ethWallet.address); + const receiverBalance = await getEthBalance(provider, ethWallet2.address); + const tx = { + to: ethWallet2.address, + value: (ethBalance + raoToEth(tao(1))).toString(), + }; + + let rejected = false; + try { + const txResponse = await ethWallet.sendTransaction(tx); + await txResponse.wait(); + } catch (error) { + rejected = true; + if (error instanceof Error) { + expect( + (error as { code?: string }).code === "INSUFFICIENT_FUNDS" || + error.message.includes("insufficient funds"), + ).toBe(true); + } + } + expect(rejected).toBe(true); + + const receiverBalanceAfterTransfer = await getEthBalance(provider, ethWallet2.address); + expect(receiverBalanceAfterTransfer).toEqual(receiverBalance); + }, + }); + + it({ + id: "T09", + title: "Transfer more than u64::max in substrate equivalent should receive error response", + test: async () => { + const receiverBalance = await getEthBalance(provider, ethWallet2.address); + const oversize = raoToEth(BigInt(2) ** BigInt(64)); + + let ethRejected = false; + try { + const txResponse = await ethWallet.sendTransaction({ + to: ethWallet2.address, + value: oversize.toString(), + }); + await txResponse.wait(); + } catch (error) { + ethRejected = true; + if (error instanceof Error) { + expect( + (error as { code?: string }).code === "INSUFFICIENT_FUNDS" || + error.message.includes("insufficient funds"), + ).toBe(true); + } + } + expect(ethRejected).toBe(true); + + const contract = new ethers.Contract( + IBALANCETRANSFER_ADDRESS, + IBalanceTransferABI, + ethWallet, + ); + let precompileRejected = false; + try { + const tx = await contract.transfer(signer.publicKey, { value: oversize.toString() }); + await tx.wait(); + } catch (error) { + precompileRejected = true; + if (error instanceof Error) { + expect( + error.message.includes("revert") || + error.message.includes("CALL_EXCEPTION"), + ).toBe(true); + } + } + expect(precompileRejected).toBe(true); + + let balanceTxRejected = false; + try { + const dest = convertH160ToSS58(ethWallet2.address); + const tx = api.tx.Balances.transfer_keep_alive({ + value: bigintToRao(BigInt(2) ** BigInt(64)), + dest: MultiAddress.Id(dest), + }); + const result = await sendTransaction(tx, signer); + balanceTxRejected = !result.success; + } catch { + balanceTxRejected = true; + } + expect(balanceTxRejected).toBe(true); + + let withdrawRejected = false; + try { + const dest = ethAddressToH160(ethWallet2.address); + const tx = api.tx.EVM.withdraw({ + value: bigintToRao(BigInt(2) ** BigInt(64)), + address: dest, + }); + const result = await sendTransaction(tx, signer); + withdrawRejected = !result.success; + } catch { + withdrawRejected = true; + } + expect(withdrawRejected).toBe(true); + + let evmCallRejected = false; + try { + const source = ethAddressToH160(ethWallet.address); + const target = ethAddressToH160(ethWallet2.address); + const tx = api.tx.EVM.call({ + source, + target, + value: [raoToEth(tao(1)), tao(0), tao(0), tao(1)], + gas_limit: BigInt(1000000), + max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], + max_priority_fee_per_gas: undefined, + input: Binary.fromText("") as unknown as Uint8Array, + nonce: undefined, + access_list: [], + authorization_list: [], + }); + const result = await sendTransaction(tx, signer); + evmCallRejected = !result.success; + } catch { + evmCallRejected = true; + } + expect(evmCallRejected).toBe(true); + + const receiverBalanceAfter = await getEthBalance(provider, ethWallet2.address); + expect(receiverBalanceAfter).toEqual(receiverBalance); + }, + }); + + it({ + id: "T10", + title: "Gas price should be 10 GWei", + test: async () => { + const feeData = await provider.getFeeData(); + expect(feeData.gasPrice).toEqual(BigInt(10000000000)); + }, + }); + + it({ + id: "T11", + title: "max_fee_per_gas and max_priority_fee_per_gas affect transaction fee properly", + test: async () => { + const testCases: [number, number, bigint][] = [ + [10, 0, BigInt(21000 * 10) * BigInt(1e9)], + [10, 10, BigInt(21000 * 10) * BigInt(1e9)], + [11, 0, BigInt(21000 * 10) * BigInt(1e9)], + ]; + + for (const [maxFeeGwei, maxPriorityGwei, expectedFee] of testCases) { + const actualFee = await transferAndGetFee( + ethWallet, + ethWallet2, + provider, + GWEI * BigInt(maxFeeGwei), + GWEI * BigInt(maxPriorityGwei), + ); + expect(actualFee).toEqual(expectedFee); + } + }, + }); + + it({ + id: "T12", + title: "Low max_fee_per_gas gets transaction rejected", + test: async () => { + let rejected = false; + try { + await transferAndGetFee( + ethWallet, + ethWallet2, + provider, + GWEI * BigInt(9), + BigInt(0), + ); + } catch (error) { + rejected = true; + if (error instanceof Error) { + expect(error.message.includes("gas price less than block base fee")).toBe(true); + } + } + expect(rejected).toBe(true); + }, + }); + + it({ + id: "T13", + title: "max_fee_per_gas lower than max_priority_fee_per_gas gets transaction rejected", + test: async () => { + let rejected = false; + try { + await transferAndGetFee( + ethWallet, + ethWallet2, + provider, + GWEI * BigInt(10), + GWEI * BigInt(11), + ); + } catch (error) { + rejected = true; + if (error instanceof Error) { + expect(error.message.includes("priorityFee cannot be more than maxFee")).toBe(true); + } + } + expect(rejected).toBe(true); + }, + }); }, }); diff --git a/ts-tests/utils/address.ts b/ts-tests/utils/address.ts index fa603e675e..be22200091 100644 --- a/ts-tests/utils/address.ts +++ b/ts-tests/utils/address.ts @@ -1,28 +1,44 @@ import { hexToU8a } from "@polkadot/util"; -import { blake2AsU8a, encodeAddress } from "@polkadot/util-crypto"; +import { blake2AsU8a, decodeAddress, encodeAddress } from "@polkadot/util-crypto"; +import { Binary } from "polkadot-api"; +import type { Address } from "viem"; -const SS58_PREFIX = 42; +const SS58_PREFIX = 42 + +export function toViemAddress(address: string): Address { + const addressNoPrefix = address.replace("0x", ""); + return `0x${addressNoPrefix}`; +} export function convertH160ToPublicKey(ethAddress: string) { const prefix = "evm:"; const prefixBytes = new TextEncoder().encode(prefix); const addressBytes = hexToU8a(ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}`); const combined = new Uint8Array(prefixBytes.length + addressBytes.length); - - // Concatenate prefix and Ethereum address combined.set(prefixBytes); combined.set(addressBytes, prefixBytes.length); + return blake2AsU8a(combined); +} - // Hash the combined data (the public key) - const hash = blake2AsU8a(combined); - return hash; +export function convertH160ToSS58(ethAddress: string): string { + return encodeAddress(convertH160ToPublicKey(ethAddress), SS58_PREFIX); } -export function convertH160ToSS58(ethAddress: string) { - // get the public key - const hash = convertH160ToPublicKey(ethAddress); +export function convertPublicKeyToSs58(publicKey: Uint8Array): string { + return encodeAddress(publicKey, SS58_PREFIX); +} + +export function ss58ToEthAddress(ss58Address: string): string { + const publicKey = decodeAddress(ss58Address); + const ethereumAddressBytes = publicKey.slice(0, 20); + return `0x${Buffer.from(ethereumAddressBytes).toString("hex")}`; +} - // Convert the hash to SS58 format - const ss58Address = encodeAddress(hash, SS58_PREFIX); - return ss58Address; +export function ss58ToH160(ss58Address: string): Binary { + const publicKey = decodeAddress(ss58Address); + return new Binary(publicKey.slice(0, 20)); } + +export function ethAddressToH160(ethAddress: string): Binary { + return new Binary(hexToU8a(ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}`)); +} \ No newline at end of file diff --git a/ts-tests/utils/balance.ts b/ts-tests/utils/balance.ts index 0ad7e701fa..53f8218e66 100644 --- a/ts-tests/utils/balance.ts +++ b/ts-tests/utils/balance.ts @@ -1,9 +1,10 @@ -import { waitForTransactionWithRetry } from "./transactions.js"; -import type { TypedApi } from "polkadot-api"; import type { subtensor } from "@polkadot-api/descriptors"; import { Keyring } from "@polkadot/keyring"; - +import type { TypedApi } from "polkadot-api"; +import { waitForTransactionWithRetry } from "./transactions.js"; export const TAO = BigInt(1000000000); // 10^9 RAO per TAO +export const GWEI = BigInt(1000000000); +export const MAX_TX_FEE = BigInt(21000000) * GWEI; export function tao(value: number): bigint { return TAO * BigInt(value); @@ -11,7 +12,11 @@ export function tao(value: number): bigint { /** Convert RAO to the EVM native balance unit (1 RAO → 1 gwei on-chain). */ export function raoToEth(rao: bigint): bigint { - return TAO * rao; + return GWEI * rao; +} + +export function bigintToRao(value: bigint): bigint { + return TAO * value; } export async function getBalance(api: TypedApi, ss58Address: string): Promise { @@ -32,5 +37,5 @@ export async function forceSetBalance( new_free: amount, }); const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); - await waitForTransactionWithRetry(api, tx, alice, "force_set_balance"); + await waitForTransactionWithRetry(api, tx, alice, "force_set_balance", 5); } diff --git a/ts-tests/utils/evm-config.ts b/ts-tests/utils/evm-config.ts new file mode 100644 index 0000000000..1d6a882f79 --- /dev/null +++ b/ts-tests/utils/evm-config.ts @@ -0,0 +1,46 @@ +/** Balance transfer precompile (same address as contract-tests). */ +export const IBALANCETRANSFER_ADDRESS = "0x0000000000000000000000000000000000000800"; + +export const IBalanceTransferABI = [ + { + inputs: [ + { + internalType: "bytes32", + name: "data", + type: "bytes32", + }, + ], + name: "transfer", + outputs: [], + stateMutability: "payable", + type: "function", + }, +] as const; + +export const WITHDRAW_CONTRACT_ABI = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; + +export const WITHDRAW_CONTRACT_BYTECODE = + "6080604052348015600e575f80fd5b506101148061001c5f395ff3fe608060405260043610601e575f3560e01c80632e1a7d4d146028576024565b36602457005b5f80fd5b603e6004803603810190603a919060b8565b6040565b005b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156082573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b609a81608a565b811460a3575f80fd5b50565b5f8135905060b2816093565b92915050565b5f6020828403121560ca5760c96086565b5b5f60d58482850160a6565b9150509291505056fea2646970667358221220f43400858bfe4fcc0bf3c1e2e06d3a9e6ced86454a00bd7e4866b3d4d64e46bb64736f6c634300081a0033"; diff --git a/ts-tests/utils/evm.ts b/ts-tests/utils/evm.ts new file mode 100644 index 0000000000..442fc030ca --- /dev/null +++ b/ts-tests/utils/evm.ts @@ -0,0 +1,29 @@ +import { subtensor } from "@polkadot-api/descriptors"; +import { Keyring } from "@polkadot/keyring"; +import { ethers } from "ethers"; +import type { TypedApi } from "polkadot-api"; +import { waitForTransactionWithRetry } from "./transactions.js"; + +export async function disableWhiteListCheck( + api: TypedApi, + disabled: boolean, +): Promise { + const value = await api.query.EVM.DisableWhitelistCheck.getValue(); + if (value === disabled) { + return; + } + + const alice = new Keyring({ type: "sr25519" }).addFromUri("//Alice"); + const internalCall = api.tx.EVM.disable_whitelist({ disabled }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "disable_whitelist", 5); +} + +export function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wallet { + const account = ethers.Wallet.createRandom(); + return new ethers.Wallet(account.privateKey, provider); +} + +export async function getEthBalance(provider: ethers.Provider, address: string): Promise { + return provider.getBalance(address); +} \ No newline at end of file diff --git a/ts-tests/utils/index.ts b/ts-tests/utils/index.ts index 3a91d860a0..956a2ce7a6 100644 --- a/ts-tests/utils/index.ts +++ b/ts-tests/utils/index.ts @@ -2,6 +2,8 @@ export * from "./account.ts"; export * from "./address.ts"; export * from "./balance.js"; export * from "./coldkey_swap.ts"; +export * from "./evm-config.ts"; +export * from "./evm.ts"; export * from "./shield_helpers.ts"; export * from "./staking.js"; export * from "./subnet.js"; diff --git a/ts-tests/utils/transactions.ts b/ts-tests/utils/transactions.ts index f64c772f79..53b9185828 100644 --- a/ts-tests/utils/transactions.ts +++ b/ts-tests/utils/transactions.ts @@ -1,10 +1,10 @@ -import { log } from "./logger.js"; import type { KeyringPair } from "@moonwall/util"; +import type { subtensor } from "@polkadot-api/descriptors"; import { sleep } from "@zombienet/utils"; -import { waitForBlocks } from "./staking.ts"; import type { Transaction, TypedApi } from "polkadot-api"; -import type { subtensor } from "@polkadot-api/descriptors"; import { getPolkadotSigner } from "polkadot-api/signer"; +import { log } from "./logger.js"; +import { waitForBlocks } from "./staking.ts"; export async function waitForTransactionWithRetry( api: TypedApi, From d4903cd3d9bb510f1d3a539e430473caf7f87b66 Mon Sep 17 00:00:00 2001 From: "subtensor-ai-review[bot]" Date: Fri, 5 Jun 2026 15:09:45 +0000 Subject: [PATCH 09/10] chore: auditor auto-fix --- ts-tests/utils/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts-tests/utils/index.ts b/ts-tests/utils/index.ts index 956a2ce7a6..c05e5dd280 100644 --- a/ts-tests/utils/index.ts +++ b/ts-tests/utils/index.ts @@ -8,4 +8,3 @@ export * from "./shield_helpers.ts"; export * from "./staking.js"; export * from "./subnet.js"; export * from "./transactions.js"; - From 519d0e48cbc77aa10c3aa75df4591f1612ba4ae9 Mon Sep 17 00:00:00 2001 From: open-junius Date: Fri, 5 Jun 2026 23:11:41 +0800 Subject: [PATCH 10/10] format code --- .../00-evm-substrate-transfer.test.ts | 54 +++++-------------- ts-tests/utils/address.ts | 4 +- ts-tests/utils/evm.ts | 7 +-- 3 files changed, 18 insertions(+), 47 deletions(-) diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index 36980228e7..93e245d276 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -26,14 +26,11 @@ import { tao, waitForFinalizedBlocks, waitForTransactionWithRetry, - WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE + WITHDRAW_CONTRACT_ABI, + WITHDRAW_CONTRACT_BYTECODE, } from "../../utils"; - -async function estimateTransactionCost( - provider: ethers.Provider, - tx: ethers.TransactionRequest, -): Promise { +async function estimateTransactionCost(provider: ethers.Provider, tx: ethers.TransactionRequest): Promise { const feeData = await provider.getFeeData(); const estimatedGas = await provider.estimateGas(tx); const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas; @@ -53,7 +50,7 @@ async function transferAndGetFee( wallet2: ethers.Wallet, provider: ethers.Provider, maxFeePerGas: bigint, - maxPriorityFeePerGas: bigint, + maxPriorityFeePerGas: bigint ): Promise { const ethBalanceBefore = await getEthBalance(provider, wallet.address); const tx = { @@ -149,11 +146,7 @@ describeSuite({ id: "T03", title: "Can transfer token from EVM to Substrate", test: async () => { - const contract = new ethers.Contract( - IBALANCETRANSFER_ADDRESS, - IBalanceTransferABI, - ethWallet, - ); + const contract = new ethers.Contract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, ethWallet); const signerSs58 = convertPublicKeyToSs58(signer.publicKey); const senderBalance = await getEthBalance(provider, ethWallet.address); @@ -245,7 +238,7 @@ describeSuite({ const contractFactory = new ethers.ContractFactory( WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE, - ethWallet, + ethWallet ); const contract = await contractFactory.deploy(); await contract.waitForDeployment(); @@ -302,7 +295,7 @@ describeSuite({ if (error instanceof Error) { expect( (error as { code?: string }).code === "INSUFFICIENT_FUNDS" || - error.message.includes("insufficient funds"), + error.message.includes("insufficient funds") ).toBe(true); } } @@ -333,7 +326,7 @@ describeSuite({ if (error instanceof Error) { expect( (error as { code?: string }).code === "INSUFFICIENT_FUNDS" || - error.message.includes("insufficient funds"), + error.message.includes("insufficient funds") ).toBe(true); } } @@ -363,17 +356,13 @@ describeSuite({ if (error instanceof Error) { expect( (error as { code?: string }).code === "INSUFFICIENT_FUNDS" || - error.message.includes("insufficient funds"), + error.message.includes("insufficient funds") ).toBe(true); } } expect(ethRejected).toBe(true); - const contract = new ethers.Contract( - IBALANCETRANSFER_ADDRESS, - IBalanceTransferABI, - ethWallet, - ); + const contract = new ethers.Contract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, ethWallet); let precompileRejected = false; try { const tx = await contract.transfer(signer.publicKey, { value: oversize.toString() }); @@ -381,10 +370,7 @@ describeSuite({ } catch (error) { precompileRejected = true; if (error instanceof Error) { - expect( - error.message.includes("revert") || - error.message.includes("CALL_EXCEPTION"), - ).toBe(true); + expect(error.message.includes("revert") || error.message.includes("CALL_EXCEPTION")).toBe(true); } } expect(precompileRejected).toBe(true); @@ -470,7 +456,7 @@ describeSuite({ ethWallet2, provider, GWEI * BigInt(maxFeeGwei), - GWEI * BigInt(maxPriorityGwei), + GWEI * BigInt(maxPriorityGwei) ); expect(actualFee).toEqual(expectedFee); } @@ -483,13 +469,7 @@ describeSuite({ test: async () => { let rejected = false; try { - await transferAndGetFee( - ethWallet, - ethWallet2, - provider, - GWEI * BigInt(9), - BigInt(0), - ); + await transferAndGetFee(ethWallet, ethWallet2, provider, GWEI * BigInt(9), BigInt(0)); } catch (error) { rejected = true; if (error instanceof Error) { @@ -506,13 +486,7 @@ describeSuite({ test: async () => { let rejected = false; try { - await transferAndGetFee( - ethWallet, - ethWallet2, - provider, - GWEI * BigInt(10), - GWEI * BigInt(11), - ); + await transferAndGetFee(ethWallet, ethWallet2, provider, GWEI * BigInt(10), GWEI * BigInt(11)); } catch (error) { rejected = true; if (error instanceof Error) { diff --git a/ts-tests/utils/address.ts b/ts-tests/utils/address.ts index be22200091..eb4b1fe905 100644 --- a/ts-tests/utils/address.ts +++ b/ts-tests/utils/address.ts @@ -3,7 +3,7 @@ import { blake2AsU8a, decodeAddress, encodeAddress } from "@polkadot/util-crypto import { Binary } from "polkadot-api"; import type { Address } from "viem"; -const SS58_PREFIX = 42 +const SS58_PREFIX = 42; export function toViemAddress(address: string): Address { const addressNoPrefix = address.replace("0x", ""); @@ -41,4 +41,4 @@ export function ss58ToH160(ss58Address: string): Binary { export function ethAddressToH160(ethAddress: string): Binary { return new Binary(hexToU8a(ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}`)); -} \ No newline at end of file +} diff --git a/ts-tests/utils/evm.ts b/ts-tests/utils/evm.ts index 442fc030ca..f8d9f6f251 100644 --- a/ts-tests/utils/evm.ts +++ b/ts-tests/utils/evm.ts @@ -4,10 +4,7 @@ import { ethers } from "ethers"; import type { TypedApi } from "polkadot-api"; import { waitForTransactionWithRetry } from "./transactions.js"; -export async function disableWhiteListCheck( - api: TypedApi, - disabled: boolean, -): Promise { +export async function disableWhiteListCheck(api: TypedApi, disabled: boolean): Promise { const value = await api.query.EVM.DisableWhitelistCheck.getValue(); if (value === disabled) { return; @@ -26,4 +23,4 @@ export function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wal export async function getEthBalance(provider: ethers.Provider, address: string): Promise { return provider.getBalance(address); -} \ No newline at end of file +}