diff --git a/test/moonwall/contracts/src/FailingConstructor.sol b/test/moonwall/contracts/src/FailingConstructor.sol new file mode 100644 index 00000000..a30781c9 --- /dev/null +++ b/test/moonwall/contracts/src/FailingConstructor.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +contract FailingConstructor { + constructor() { + require(false); + } +} diff --git a/test/moonwall/contracts/src/StorageLoop.sol b/test/moonwall/contracts/src/StorageLoop.sol new file mode 100644 index 00000000..fcab6859 --- /dev/null +++ b/test/moonwall/contracts/src/StorageLoop.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.2 <0.9.0; + +contract StorageLoop { + mapping(uint256 => uint256) public map; + mapping(uint256 => uint256) public map2; + + function store(uint16 n) public { + for (uint16 i = 0; i < n; i++) { + map[i] = i + 1; + } + } + + function store2(uint256 i) public { + map2[i] = i; + } +} diff --git a/test/moonwall/suites/dev/common/test-proxy/test-proxy-balance.ts b/test/moonwall/suites/dev/common/test-proxy/test-proxy-balance.ts new file mode 100644 index 00000000..371bb80d --- /dev/null +++ b/test/moonwall/suites/dev/common/test-proxy/test-proxy-balance.ts @@ -0,0 +1,80 @@ +import { describeSuite, expect } from "@moonwall/cli"; +import { + ALITH_ADDRESS, + CHARLETH_ADDRESS, + baltathar, +} from "@moonwall/util"; + +describeSuite({ + id: "D010502", + title: "Proxy: Balances", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should accept known proxy", + test: async () => { + const beforeCharlieBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + const { result } = await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(baltathar.address, "Balances" as any, 0) + ); + const proxyAdded = result!.events.find( + ({ event }) => event.method === "ProxyAdded" + ); + expect(proxyAdded).to.not.be.undefined; + expect(proxyAdded!.event.data[2].toString()).to.be.eq("Balances"); //ProxyType + expect(result!.events.some(({ event }) => event.method === "ExtrinsicSuccess")).to.be.true; + + const { result: result2 } = await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxy( + ALITH_ADDRESS, + null, + context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100) + ) + .signAsync(baltathar) + ); + + const proxyExecuted = result2!.events.find( + ({ event }) => event.method === "ProxyExecuted" + ); + expect(proxyExecuted).to.not.be.undefined; + expect(proxyExecuted!.event.data[0].toString()).to.be.eq("Ok"); + expect(result2!.events.some(({ event }) => event.method === "ExtrinsicSuccess")).to.be + .true; + const afterCharlieBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlieBalance - beforeCharlieBalance).to.be.eq(100n); + }, + }); + + it({ + id: "T02", + title: "shouldn't accept other proxy types", + test: async () => { + await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(baltathar.address, "Balances", 0) + ); + + const { result } = await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxy( + ALITH_ADDRESS, + null, + context.polkadotJs().tx.system.remark("0x") + ) + .signAsync(baltathar) + ); + + const proxyExecuted = result!.events.find( + ({ event }) => event.method === "ProxyExecuted" + ); + expect(proxyExecuted).to.not.be.undefined; + // Balances proxy type should not allow system.remark + expect(proxyExecuted!.event.data[0].toString()).to.not.be.eq("Ok"); + expect(result!.events.some(({ event }) => event.method === "ExtrinsicSuccess")).to.be.true; + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/multisig/test-multisigs.ts b/test/moonwall/suites/dev/stagenet/multisig/test-multisigs.ts new file mode 100644 index 00000000..f760607b --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/multisig/test-multisigs.ts @@ -0,0 +1,184 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { blake2AsHex, createKeyMulti } from "@polkadot/util-crypto"; +import { u8aToHex } from "@polkadot/util"; +import { + ALITH_ADDRESS, + BALTATHAR_ADDRESS, + CHARLETH_ADDRESS, + DOROTHY_ADDRESS, + alith, + baltathar, +} from "@moonwall/util"; + +// This test cases in this suite are dependent on each other, and must be run in order. +// TODO: Make the test cases atomic + +describeSuite({ + id: "D022101", + title: "Multisigs - perform multisigs operations", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let threshold: number; + let call: any; + let encodedCall: string; + let encodedCallHash: string; + + // multisig accountId + let encodedMultisigId: Uint8Array; + let multisigId: string; + + beforeAll(async function () { + // set threshold and create multisig accountId + threshold = 2; + encodedMultisigId = createKeyMulti([ALITH_ADDRESS, BALTATHAR_ADDRESS, CHARLETH_ADDRESS], 2); + multisigId = u8aToHex(encodedMultisigId.slice(0, 20)); + + // encode and hash the call we want to dispatch as a multisig operation + call = context.polkadotJs().tx.balances.transferKeepAlive(DOROTHY_ADDRESS, 20); + encodedCall = call.method.toHex(); + encodedCallHash = blake2AsHex(encodedCall); + }); + + it({ + id: "T01", + title: "Should create a multisig operation with asMulti", + test: async () => { + // set signatories + const otherSignatories = [BALTATHAR_ADDRESS, CHARLETH_ADDRESS]; + const block = await context.createBlock( + context + .polkadotJs() + .tx.multisig.asMulti(threshold, otherSignatories, null, encodedCall, {}) + .signAsync(alith) + ); + + // check the event 'NewMultisig' was emitted + const records = await context.polkadotJs().query.system.events(); + const events = records.filter( + ({ event }) => event.section === "multisig" && event.method === "NewMultisig" + ); + expect(events).to.have.lengthOf(1); + expect(block.result!.successful).to.be.true; + }, + }); + + it({ + id: "T02", + title: "Should be able to approve a multisig operation with approveAsMulti", + test: async function () { + // signatories (sorted) + const otherSignatories = [CHARLETH_ADDRESS, ALITH_ADDRESS]; + // create a new multisig operation + await context.createBlock( + context + .polkadotJs() + .tx.multisig.asMulti(threshold, otherSignatories, null, encodedCall, {}) + .signAsync(alith), + { allowFailures: true } + ); + + // take the info of the new multisig operation saved in storage + const multisigInfo = await context + .polkadotJs() + .query.multisig.multisigs(multisigId, encodedCallHash); + const block = await context.createBlock( + context + .polkadotJs() + .tx.multisig.approveAsMulti( + threshold, + otherSignatories, + multisigInfo.unwrap().when, + encodedCallHash, + {} + ) + .signAsync(baltathar), + { allowFailures: true } + ); + + // check the event 'MultisigApproval' was emitted + const records = await context.polkadotJs().query.system.events(); + const events = records.filter( + ({ event }) => event.section === "multisig" && event.method === "MultisigApproval" + ); + expect(events).to.have.lengthOf(1); + expect(block.result!.successful).to.be.true; + }, + }); + + it({ + id: "T03", + title: "Should be able to cancel a multisig operation", + test: async () => { + const otherSignatories = [BALTATHAR_ADDRESS, CHARLETH_ADDRESS]; + // create a new multisig operation + await context.createBlock( + context + .polkadotJs() + .tx.multisig.asMulti(threshold, otherSignatories, null, encodedCall, {}) + .signAsync(alith), + { allowFailures: true } + ); + + // take the info of the new multisig operation saved in storage + const multisigInfo = await context + .polkadotJs() + .query.multisig.multisigs(multisigId, encodedCallHash); + const block = await context.createBlock( + context + .polkadotJs() + .tx.multisig.cancelAsMulti( + threshold, + otherSignatories, + multisigInfo.unwrap().when, + encodedCallHash + ) + .signAsync(alith) + ); + + const records = await context.polkadotJs().query.system.events(); + const events = records.filter( + ({ event }) => event.section === "multisig" && event.method === "MultisigCancelled" + ); + expect(events, "event 'MultisigCancelled' was not emitted").to.have.lengthOf(1); + expect(block.result!.successful).to.be.true; + }, + }); + + it({ + id: "T04", + title: "Should fail if signatories are out of order", + test: async () => { + const otherSignatories = [CHARLETH_ADDRESS, BALTATHAR_ADDRESS]; + const block = await context.createBlock( + context + .polkadotJs() + .tx.multisig.asMulti(threshold, otherSignatories, null, encodedCall, {}) + .signAsync(alith), + { allowFailures: true } + ); + expect(block.result!.error!.name, "signatories (they are not sorted)").to.equal( + "SignatoriesOutOfOrder" + ); + expect(block.result!.successful).to.be.false; + }, + }); + + it({ + id: "T05", + title: "Should fail if sender is present in signatories", + test: async () => { + // signatories (with sender in signatories) + const otherSignatories = [ALITH_ADDRESS, BALTATHAR_ADDRESS]; + const block = await context.createBlock( + context + .polkadotJs() + .tx.multisig.asMulti(threshold, otherSignatories, null, encodedCall, {}) + .signAsync(alith), + { allowFailures: true } + ); + expect(block.result!.error!.name).to.equal("SenderInSignatories"); + expect(block.result!.successful).to.be.false; + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/node-rpc/test-node-rpc-peer.ts b/test/moonwall/suites/dev/stagenet/node-rpc/test-node-rpc-peer.ts new file mode 100644 index 00000000..e699fd94 --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/node-rpc/test-node-rpc-peer.ts @@ -0,0 +1,21 @@ +import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; + +describeSuite({ + id: "D022201", + title: "Node - RPC", + foundationMethods: "dev", + testCases: ({ context, it }) => { + it({ + id: "T01", + title: "should report peer count in hex", + test: async function () { + // this tests that the "net_peerCount" response comes back in hex and not decimal. + // related: frontier commits 677548c and 78fb3bc + const result = await customDevRpcRequest("net_peerCount", []); + + expect(result).to.be.equal("0x0"); + expect(typeof result).to.be.equal("string"); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/polkadot-js/test-polkadot-api.ts b/test/moonwall/suites/dev/stagenet/polkadot-js/test-polkadot-api.ts new file mode 100644 index 00000000..524a16c3 --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/polkadot-js/test-polkadot-api.ts @@ -0,0 +1,141 @@ +import { describeSuite, expect } from "@moonwall/cli"; +import { ALITH_ADDRESS, GLMR, generateKeyringPair } from "@moonwall/util"; + +describeSuite({ + id: "D022501", + title: "Polkadot API", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should return genesis block", + test: async function () { + const lastHeader = await context.polkadotJs().rpc.chain.getHeader(); + expect(Number(lastHeader.number) >= 0).to.be.true; + }, + }); + + it({ + id: "T02", + title: "should return latest header number", + test: async function () { + await context.createBlock(); + const lastHeader = await context.polkadotJs().rpc.chain.getHeader(); + expect(lastHeader.number.toNumber()).to.be.at.least(1); + }, + }); + + it({ + id: "T03", + title: "transfers should be stored on chain", + test: async function () { + const randomAddress = generateKeyringPair().address as `0x${string}`; + await context.createBlock( + context.polkadotJs().tx.balances.transferAllowDeath(randomAddress, 2n * GLMR) + ); + + expect(BigInt(await context.viem().getBalance({ address: randomAddress }))).to.equal( + 2n * GLMR + ); + }, + }); + + it({ + id: "T04", + title: "should appear in extrinsics", + test: async function () { + const randomAddress = generateKeyringPair().address as `0x${string}`; + await context.createBlock( + context.polkadotJs().tx.balances.transferAllowDeath(randomAddress, 2n * GLMR) + ); + const signedBlock = await context.polkadotJs().rpc.chain.getBlock(); + + // Expecting 3 extrinsics so far: + // timestamp, author, the parachain validation data, randomness, and the balances transfer. + expect(signedBlock.block.extrinsics).to.be.of.length(3); + + signedBlock.block.extrinsics.forEach((ex, index) => { + const { + method: { args, method, section }, + } = ex; + const message = `${section}.${method}(${args.map((a) => a.toString()).join(", ")})`; + + switch (index) { + case 0: + expect(message.substring(0, 13)).to.eq(`timestamp.set`); + break; + case 1: + expect(message.toLocaleLowerCase()).to.match(/^randomness\.setbaberandomness/); + break; + case 2: + expect(message).to.eq( + `balances.transferAllowDeath(${randomAddress}, 2000000000000000000)` + ); + expect(ex.signer.toString()).to.eq(ALITH_ADDRESS); + break; + default: + throw new Error(`Unexpected extrinsic: ${message}`); + } + }); + }, + }); + + it({ + id: "T05", + title: "should appear in events", + test: async function () { + // Generating two transfers to ensure treasury account exists + const randomAddress = generateKeyringPair().address as `0x${string}`; + await context.createBlock( + context.polkadotJs().tx.balances.transferAllowDeath(randomAddress, 2n * GLMR) + ); + + const randomAddress2 = generateKeyringPair().address as `0x${string}`; + await context.createBlock( + context.polkadotJs().tx.balances.transferAllowDeath(randomAddress2, 2n * GLMR) + ); + const signedBlock = await context.polkadotJs().rpc.chain.getBlock(); + const apiAt = await context.polkadotJs().at(signedBlock.block.header.hash); + const allRecords = await apiAt.query.system.events(); + + // map between the extrinsics and events + signedBlock.block.extrinsics.forEach((_, index) => { + // filter the specific events based on the phase and then the + // index of our extrinsic in the block + const events = allRecords + .filter(({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index)) + .map(({ event }) => event); + + switch (index) { + // First 2 events: + // timestamp.set:: system.ExtrinsicSuccess + // randomness.setBabeRandomness:: system.ExtrinsicSuccess + case 0: + case 1: + expect(events).to.be.of.length(1); + expect(context.polkadotJs().events.system.ExtrinsicSuccess.is(events[0])).to.be.true; + break; + // balances.transferAllowDeath emits: system.NewAccount, balances.Endowed, + // balances.Transfer, (other events), system.ExtrinsicSuccess + case 2: + log(events.map((e) => `${e.section}.${e.method}`).join(" - ")); + expect(events.length).to.be.at.least(7); + expect(events.some((e) => context.polkadotJs().events.system.NewAccount.is(e))).to.be + .true; + expect(events.some((e) => context.polkadotJs().events.balances.Endowed.is(e))).to.be + .true; + expect(events.some((e) => context.polkadotJs().events.balances.Transfer.is(e))).to.be + .true; + // ExtrinsicSuccess should be the last event + expect( + context.polkadotJs().events.system.ExtrinsicSuccess.is(events[events.length - 1]) + ).to.be.true; + break; + default: + throw new Error(`Unexpected extrinsic`); + } + }); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/polkadot-js/test-polkadot-chain-info.ts b/test/moonwall/suites/dev/stagenet/polkadot-js/test-polkadot-chain-info.ts new file mode 100644 index 00000000..64b5455a --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/polkadot-js/test-polkadot-chain-info.ts @@ -0,0 +1,21 @@ +import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; + +describeSuite({ + id: "D022502", + title: "Web3Api Information", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should include client version", + test: async function () { + const version = (await customDevRpcRequest("web3_clientVersion", [])) as string; + const specName = context.polkadotJs().runtimeVersion.specName.toString(); + const specVersion = context.polkadotJs().runtimeVersion.specVersion.toString(); + const implVersion = context.polkadotJs().runtimeVersion.implVersion.toString(); + const expected = `${specName}/v${specVersion}.${implVersion}/fc-rpc-2.0.0-dev`; + expect(version).toBe(expected); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/precompile/test-precompile-bn128-bounds.ts b/test/moonwall/suites/dev/stagenet/precompile/test-precompile-bn128-bounds.ts new file mode 100644 index 00000000..4ee0e5b8 --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/precompile/test-precompile-bn128-bounds.ts @@ -0,0 +1,80 @@ +import { describeSuite } from "@moonwall/cli"; +import { createViemTransaction, sendRawTransaction } from "@moonwall/util"; +/* + * These test cases trigger bugs in the bn128 precompiles which perform a from_slice() + * call on unchecked input. + * + * Fixed by: + * https://github.com/paritytech/frontier/pull/394 + */ + +describeSuite({ + id: "D022703", + title: "Precompiles - bn128 bounds", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should fail gracefully (case 1)", + test: async () => { + // some call data which will cause bn128 to be called with insufficient input. this + // presumably was generated through fuzzing. + const data = + ("0x608060405234801561001057600080fd5b5060008060405180608001604052807f2243525c5eda" + + "1401003c45ac0ca3fe4dd85e830a4ce6b65fa1eeaee202839703815260207f6e01001d33be6da800" + + "00002bcc35964723180eed75f91a010001007d48f195c91581526020017f18b18acfb4c2c30276db" + + "5411000000000000000b4691610c5d3b00010001b17f81526020017f063c909c4720840cb5134cb9" + + "f546c80200579d040100d32efc0d288197f37266815246475b6100bf61044d505b6040604b808460" + + "006007600019f1925082610142576000000000acd401000000000000000000000000000000000000" + + "000000000000000000008152600401808060200182810300192c748252601e8152602001887f656c" + + "6c967074000381327572766520616464ad74696f6e206661696c6564000081525060200191500000" + + "000000000009fd5b7f2bd3e6d0f3b142924f5ca7b49ce5b9d585420400ae5648e61d02268b1a0a9f" + + "b7816000600202020202020202020202020202020202fd0202020203020202020202020202020202" + + "0202fb02020a02020202020202020202020202020202020202020202020202020202020202020202" + + "02020202020200000000000000000a1c000000000000000000000000000000000000000901010037" + + "190100000000000000000000f81a0100000002020202020202020202020202020202020202028a30" + + "a82123b27db75200aedc4a45a0e84fbd1f9f3621350bb778119630350eb7a7e613058daf51e9f514" + + "8ea65715eaac3d8019f80498112fc4860a020202020202020202fd02020202020202020202020202" + + "02020202020202020202020202020202020202020202020202020202020202020202020202020212" + + "02020202020202020202010202fd0202020202020202020202020202020202020202020202020202" + + "020202020202020202020202005f02d2020202020202020202020202020202020202020202020202" + + "02020202020202020202020302020202020202020202020202020202020202020202020202020202" + + "0302020202020202020202020202") as `0x${string}`; + + const tx = await createViemTransaction(context, { + to: "0x0000000000000000000000000000000000000007", + data, + skipEstimation: true, + }); + + await sendRawTransaction(context, tx); + + // we expect that the node hasn't crashed by here. without a fix, the previous web3 request + // would have been sufficient to crash our node. now it fails with "ExhaustsResources". if + // we can create a block, we must not have crashed. + await context.createBlock(); + }, + }); + + it({ + id: "T02", + title: "should fail gracefully (case 2)", + test: async () => { + // similar to the above call data, although triggers a slightly different bug + + const tx = await createViemTransaction(context, { + to: "0x0000000000000000000000000000000000000007", + data: "0x0000000000000000000000000000000000000000050000000000008303d0300d901401", + skipEstimation: true, + }); + + await sendRawTransaction(context, tx); + + // we expect that the node hasn't crashed by here. without a fix, the previous web3 request + // would have been sufficient to crash our node. now it fails with "ExhaustsResources". if + // we can create a block, we must not have crashed. + await context.createBlock(); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/proxy/test-proxy.ts b/test/moonwall/suites/dev/stagenet/proxy/test-proxy.ts new file mode 100644 index 00000000..5c313bef --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/proxy/test-proxy.ts @@ -0,0 +1,257 @@ +import { beforeEach, describeSuite, expect } from "@moonwall/cli"; +import { + ALITH_ADDRESS, + CHARLETH_ADDRESS, + type KeyringPair, + alith, + generateKeyringPair, +} from "@moonwall/util"; + +// In these tests Alith will allow signer to perform calls on her behalf. +// Charleth is used as a target account when making transfers. + +describeSuite({ + id: "D022902", + title: "Proxy - proxy", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let signer: KeyringPair; + + beforeEach(async () => { + signer = generateKeyringPair("ethereum"); + + await context.createBlock( + context.polkadotJs().tx.balances.transferAllowDeath(signer.address, 5n * 10n ** 18n) + ); + }); + + it({ + id: "T01", + title: "shouldn't accept unknown proxy", + test: async function () { + const beforeCharlethBalance = await context + .viem() + .getBalance({ address: CHARLETH_ADDRESS }); + + const expectEvents = [context.polkadotJs().events.system.ExtrinsicFailed]; + + await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxy( + ALITH_ADDRESS, + null, + context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100) + ) + .signAsync(signer), + { expectEvents, signer: alith, allowFailures: true } + ); + const afterCharlethBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlethBalance - beforeCharlethBalance).to.be.eq(0n); + }, + }); + + it({ + id: "T02", + title: "should accept known proxy", + test: async () => { + const beforeCharlethBalance = await context + .viem() + .getBalance({ address: CHARLETH_ADDRESS }); + + const events1 = [ + context.polkadotJs().events.system.ExtrinsicSuccess, + context.polkadotJs().events.proxy.ProxyAdded, + ]; + + const { result } = await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(signer.address, "Any", 0), + { signer: alith, expectEvents: events1 } + ); + const proxyAdded = result?.events.find(({ event }) => + context.polkadotJs().events.proxy.ProxyAdded.is(event) + ); + expect(proxyAdded).to.not.be.undefined; + expect(proxyAdded!.event.data[2].toString()).to.be.eq("Any"); //ProxyType + + const events2 = [ + context.polkadotJs().events.system.ExtrinsicSuccess, + context.polkadotJs().events.proxy.ProxyExecuted, + ]; + + const { result: result2 } = await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxy( + alith.address, + null, + context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100) + ) + .signAsync(signer), + { signer: alith, expectEvents: events2 } + ); + const proxyExecuted = result2?.events.find(({ event }) => + context.polkadotJs().events.proxy.ProxyExecuted.is(event) + ); + expect(proxyExecuted).to.not.be.undefined; + expect(proxyExecuted!.event.data[0].toString()).to.be.eq("Ok"); + const afterCharlethBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlethBalance - beforeCharlethBalance).to.be.eq(100n); + }, + }); + + it({ + id: "T03", + title: "shouldn't accept removed proxy", + test: async () => { + const beforeCharlethBalance = await context + .viem() + .getBalance({ address: CHARLETH_ADDRESS }); + + await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(signer.address, "Any", 0), + { signer: alith, allowFailures: false } + ); + + await context.createBlock( + context.polkadotJs().tx.proxy.removeProxy(signer.address, "Any", 0), + { signer: alith, allowFailures: false } + ); + + await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxy( + alith.address, + null, + context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100) + ) + .signAsync(signer), + { signer: alith, expectEvents: [context.polkadotJs().events.system.ExtrinsicFailed] } + ); + const afterCharlethBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlethBalance - beforeCharlethBalance).to.be.eq(0n); + }, + }); + + it({ + id: "T04", + title: "shouldn't accept instant for delayed proxy", + test: async () => { + const beforeCharlethBalance = await context + .viem() + .getBalance({ address: CHARLETH_ADDRESS }); + + await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(signer.address, "Any", 2), + { signer: alith, allowFailures: false } + ); + + await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxy( + alith.address, + null, + context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100) + ) + .signAsync(signer), + { signer: alith, expectEvents: [context.polkadotJs().events.system.ExtrinsicFailed] } + ); + const afterCharlethBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlethBalance - beforeCharlethBalance).to.be.eq(0n); + }, + }); + + it({ + id: "T05", + title: "shouldn't accept early delayed proxy", + test: async () => { + const beforeCharlethBalance = await context + .viem() + .getBalance({ address: CHARLETH_ADDRESS }); + const { result } = await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(signer.address, "Any", 6), + { signer: alith, allowFailures: false } + ); + result?.events.forEach(({ event }) => log(`1${event.method}(${event.data})`)); + + const transfer = context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100); + + const { result: result2 } = await context.createBlock( + context.polkadotJs().tx.proxy.announce(alith.address, transfer.hash).signAsync(signer), + { + signer: alith, + expectEvents: [context.polkadotJs().events.proxy.Announced], + allowFailures: false, + } + ); + result2?.events.forEach(({ event }) => log(`2${event.method}(${event.data})`)); + + const { result: result3 } = await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxyAnnounced(signer.address, alith.address, null, transfer) + .signAsync(signer), + { + signer: alith, + expectEvents: [context.polkadotJs().events.system.ExtrinsicFailed], + } + ); + result3?.events.forEach(({ event }) => log(`3${event.method}(${event.data})`)); + const afterCharlethBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlethBalance - beforeCharlethBalance).to.be.eq(0n); + }, + }); + + it({ + id: "T06", + title: "should accept on-time delayed proxy ", + test: async () => { + const beforeCharlethBalance = await context + .viem() + .getBalance({ address: CHARLETH_ADDRESS }); + await context.createBlock( + context.polkadotJs().tx.proxy.addProxy(signer.address, "Any", 2), + { signer: alith, allowFailures: false } + ); + + const transfer = context.polkadotJs().tx.balances.transferAllowDeath(CHARLETH_ADDRESS, 100); + const u8a = transfer.method.toU8a(); + const transfer_hash = transfer.registry.hash(u8a).toHex(); + + const { result: result2 } = await context.createBlock( + context.polkadotJs().tx.proxy.announce(alith.address, transfer_hash).signAsync(signer), + { + signer: alith, + expectEvents: [context.polkadotJs().events.proxy.Announced], + allowFailures: false, + } + ); + const announced = result2?.events.find(({ event }) => + context.polkadotJs().events.proxy.Announced.is(event) + ); + expect(announced).to.not.be.undefined; + expect(announced!.event.data[2].toHex()).to.eq(transfer_hash); + + await context.createBlock(); + await context.createBlock(); + + // On time. + const { result: result3 } = await context.createBlock( + context + .polkadotJs() + .tx.proxy.proxyAnnounced(signer.address, alith.address, null, transfer) + .signAsync(signer), + { + signer: alith, + expectEvents: [context.polkadotJs().events.proxy.ProxyExecuted], + allowFailures: false, + } + ); + const afterCharlethBalance = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); + expect(afterCharlethBalance - beforeCharlethBalance).to.be.eq(100n); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/receipt/test-receipt-revert.ts b/test/moonwall/suites/dev/stagenet/receipt/test-receipt-revert.ts new file mode 100644 index 00000000..37890c0d --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/receipt/test-receipt-revert.ts @@ -0,0 +1,33 @@ +import { describeSuite, expect } from "@moonwall/cli"; +import { getAddress } from "viem"; + +describeSuite({ + id: "D023101", + title: "Receipt - Revert", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should generate a receipt for a reverted transaction", + test: async function () { + const { hash } = await context.deployContract!("FailingConstructor", { gas: 300000n }); + const receipt = await context.viem().getTransactionReceipt({ hash }); + + expect(receipt.status).toBe("reverted"); + expect(receipt.blockNumber).toBe(1n); + expect(getAddress(receipt.contractAddress!)).toBe( + getAddress("0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3") + ); + expect(receipt.cumulativeGasUsed).toBe(54604n); + expect(getAddress(receipt.from!)).toBe( + getAddress("0xf24ff3a9cf04c71dbc94d0b566f7a27b94566cac") + ); + expect(receipt.gasUsed).toBe(54604n); + expect(receipt.logs).toStrictEqual([]); + expect(receipt.transactionHash).toBe(hash); + expect(receipt.to).toBe(null); + expect(receipt.transactionIndex).toBe(0); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/receipt/test-receipt.ts b/test/moonwall/suites/dev/stagenet/receipt/test-receipt.ts new file mode 100644 index 00000000..130e70d4 --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/receipt/test-receipt.ts @@ -0,0 +1,61 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { BALTATHAR_ADDRESS, alith } from "@moonwall/util"; + +describeSuite({ + id: "D023103", + title: "Receipt - Contract", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let txHash: string; + let eventContract: `0x${string}`; + + beforeAll(async () => { + const { contractAddress, hash } = await context.deployContract!("EventEmitter"); + eventContract = contractAddress; + txHash = hash; + }); + + it({ + id: "T01", + title: "Should generate receipt", + test: async function () { + const block = await context.viem().getBlock({ blockNumber: 1n }); + const receipt = await context + .viem() + .getTransactionReceipt({ hash: txHash as `0x${string}` }); + + expect(receipt.blockHash).toBe(block.hash); + expect(receipt.blockNumber).toBe(block.number); + expect(receipt.from).toBe(alith.address.toLowerCase()); + expect(receipt.logs.length).toBe(1); + expect(receipt.logs[0].address).toBe(eventContract); + expect(receipt.logs[0].blockHash).toBe(block.hash); + }, + }); + + it({ + id: "T02", + title: "should calculate effective gas price", + test: async function () { + const maxFeePerGas = 10_000_000_000n * 2n; + + const rawTxn = await context.createTxn!({ + gas: 21000n, + libraryType: "viem", + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxFeePerGas, + to: BALTATHAR_ADDRESS, + data: "0x", + txnType: "eip1559", + }); + await context.createBlock(rawTxn); + + const block = await context.viem().getBlock(); + const receipt = await context + .viem() + .getTransactionReceipt({ hash: block.transactions[0] as `0x${string}` }); + expect(receipt.effectiveGasPrice).to.be.eq(maxFeePerGas); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/storage-growth/test-evm-store-storage-growth.ts b/test/moonwall/suites/dev/stagenet/storage-growth/test-evm-store-storage-growth.ts new file mode 100644 index 00000000..964dd79f --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/storage-growth/test-evm-store-storage-growth.ts @@ -0,0 +1,76 @@ +// TODO: Constants (storage growth ratios, limits) may need adjustment for DataHaven's runtime configuration +import { + TransactionTypes, + beforeAll, + deployCreateCompiledContract, + describeSuite, +} from "@moonwall/cli"; +import { createEthersTransaction } from "@moonwall/util"; +import { expectEVMResult, expectOk } from "../../../../helpers"; +import { type Abi, encodeFunctionData } from "viem"; + +describeSuite({ + id: "D023403", + title: "Storage growth limit - New Entries", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let storageLoopAddress: `0x${string}`; + let storageLoopAbi: Abi; + // Number of bytes added to storage for a new entry. + const ACCOUNT_STORAGE_SIZE = 116; + // Ratio of gas to storage growth. (BlockGasLimit (15_000_000) / BlockStorageLimit (40kb)) + const GAS_LIMIT_STORAGE_GROWTH_RATIO = 366; + beforeAll(async () => { + const { contractAddress, abi } = await deployCreateCompiledContract(context, "StorageLoop"); + storageLoopAddress = contractAddress; + storageLoopAbi = abi; + + await context.createBlock(); + }); + + for (const txnType of TransactionTypes) { + it({ + id: `T0${TransactionTypes.indexOf(txnType) + 1}`, + title: "should out of gas when gas provided is not enough to cover storage growth", + test: async function () { + // Tx is creating 5 new storage entries. So, required gas is: + // (5 * ACCOUNT_STORAGE_SIZE) * GAS_LIMIT_STORAGE_GROWTH_RATIO = 212_280 + // Execute tx with insufficient gas limit + const rawSigned = await createEthersTransaction(context, { + to: storageLoopAddress, + data: encodeFunctionData({ + abi: storageLoopAbi, + functionName: "store", + // for each transaction type, we add 5 new storage entries + args: [5 + 5 * TransactionTypes.indexOf(txnType)], + }), + gasLimit: 212_270, + }); + + const { result } = await context.createBlock(rawSigned); + // Check that the transaction failed with an out of gas error + expectEVMResult(result!.events, "Error", "OutOfGas"); + }, + }); + + it({ + id: `T0${TransactionTypes.indexOf(txnType) + 4}`, + title: "should successfully execute when updating existing storage entries (no growth)", + test: async function () { + // Update 5 existing storage entries. So, required gas should be less than 212_280 + const rawSigned = await createEthersTransaction(context, { + to: storageLoopAddress, + data: encodeFunctionData({ + abi: storageLoopAbi, + functionName: "store", + args: [5], + }), + gasLimit: 50_000, + }); + + await expectOk(context.createBlock(rawSigned)); + }, + }); + } + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/sudo/test-sudo.ts b/test/moonwall/suites/dev/stagenet/sudo/test-sudo.ts new file mode 100644 index 00000000..b134f7cb --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/sudo/test-sudo.ts @@ -0,0 +1,123 @@ +import { describeSuite, expect } from "@moonwall/cli"; +import { + ALITH_ADDRESS, + DEFAULT_GENESIS_BALANCE, + baltathar, + generateKeyringPair, +} from "@moonwall/util"; +import { ALITH_GENESIS_TRANSFERABLE_BALANCE, verifyLatestBlockFees } from "../../../../helpers"; +import { CHARLETH_ADDRESS, ETHAN_ADDRESS } from "@moonwall/util"; + +describeSuite({ + id: "D023601", + title: "Sudo - successful forceSetBalance", + foundationMethods: "dev", + testCases: ({ context, it }) => { + it({ + id: "T01", + title: "should be able to call sudo with the right account", + test: async function () { + const { result } = await context.createBlock( + context + .polkadotJs() + .tx.sudo.sudo(context.polkadotJs().tx.balances.forceSetBalance(ETHAN_ADDRESS, 0)) + ); + + const account = await context.polkadotJs().query.system.account(ETHAN_ADDRESS); + expect(account.data.free.toBigInt()).toBe(0n); + + // DataHaven has 1 more event (NewChallengeSeed) than Moonbeam, + // coming from the StorageHub ProofsDealer pallet. + expect(result!.events.length).to.eq(8); + console.log(result!.events.map((e) => e.event.toHuman())); + expect(context.polkadotJs().events.balances.BalanceSet.is(result!.events[3].event)).to.be + .true; + expect(context.polkadotJs().events.sudo.Sudid.is(result!.events[4].event)).to.be.true; + expect(context.polkadotJs().events.balances.Deposit.is(result!.events[5].event)).to.be.true; + expect(context.polkadotJs().events.system.ExtrinsicSuccess.is(result!.events[7].event)).to + .be.true; + + expect( + await context.viem().getBalance({ address: ALITH_ADDRESS }), + "diff should be null for sudo - funds are sent back" + ).to.equal(ALITH_GENESIS_TRANSFERABLE_BALANCE); + }, + }); + + it({ + id: "T02", + title: "should charge the correct amount of gas when calling sudo", + test: async function () { + await context.createBlock( + context + .polkadotJs() + .tx.sudo.sudo( + context + .polkadotJs() + .tx.balances.forceSetBalance(CHARLETH_ADDRESS, DEFAULT_GENESIS_BALANCE) + ) + ); + + await verifyLatestBlockFees(context); + }, + }); + + it({ + id: "T03", + title: "should NOT be able to call sudo with another account than sudo account", + test: async function () { + const baltathar_before = await context.polkadotJs().query.system.account(CHARLETH_ADDRESS); + const { result } = await context.createBlock( + context + .polkadotJs() + .tx.sudo.sudo(context.polkadotJs().tx.balances.forceSetBalance(CHARLETH_ADDRESS, 0)) + .signAsync(baltathar), + { allowFailures: true } + ); + + // Check that balance didn't change + const account = await context.polkadotJs().query.system.account(CHARLETH_ADDRESS); + expect(account.data.free.toBigInt()).toBe(DEFAULT_GENESIS_BALANCE); + + expect(result!.events.length === 8).to.be.true; + expect(context.polkadotJs().events.system.NewAccount.is(result!.events[3].event)).to.be + .true; + expect(context.polkadotJs().events.balances.Endowed.is(result!.events[4].event)).to.be.true; + expect(context.polkadotJs().events.system.ExtrinsicFailed.is(result!.events[7].event)).to.be + .true; + }, + }); + + it({ + id: "T04", + title: "should not be able to call sudo with no funds", + test: async function () { + const newSigner = generateKeyringPair(); + + await context.createBlock(context.polkadotJs().tx.sudo.setKey(newSigner.address), { + allowFailures: false, + }); + + expect((await context.polkadotJs().query.sudo.key()).unwrap().toString()).toBe( + newSigner.address + ); + + await expect( + async () => + await context.createBlock( + context + .polkadotJs() + .tx.sudo.sudo(context.polkadotJs().tx.balances.forceSetBalance(CHARLETH_ADDRESS, 0)) + .signAsync(newSigner) + ) + ).rejects.toThrowError( + "1010: Invalid Transaction: Inability to pay some fees , e.g. account balance too low" + ); + + expect(await context.viem().getBalance({ address: CHARLETH_ADDRESS })).to.equal( + DEFAULT_GENESIS_BALANCE + ); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/txpool/test-txpool-future.ts b/test/moonwall/suites/dev/stagenet/txpool/test-txpool-future.ts new file mode 100644 index 00000000..6f6ced8f --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/txpool/test-txpool-future.ts @@ -0,0 +1,69 @@ +import { beforeAll, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli"; +import { alith, createEthersTransaction, sendRawTransaction } from "@moonwall/util"; +import { encodeDeployData, toHex } from "viem"; + +describeSuite({ + id: "D023802", + title: "TxPool - Future Ethereum transaction", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let txHash: string; + let deployData: string; + + beforeAll(async () => { + const { abi, bytecode } = fetchCompiledContract("MultiplyBy7"); + deployData = encodeDeployData({ + abi, + bytecode, + }); + + const rawTxn = await createEthersTransaction(context, { + data: deployData, + gasLimit: 1048576, + nonce: 1, // future nonce + }); + txHash = await sendRawTransaction(context, rawTxn); + }); + + it({ + id: "T01", + title: "should appear in the txpool inspection", + test: async function () { + const inspect = (await context + .viem() + .transport.request({ method: "txpool_inspect" })) as any; + // web3 rpc returns lowercase + const data = inspect.queued[alith.address.toLowerCase()][toHex(1)]; + expect(data).to.not.be.undefined; + expect(data).to.be.equal( + "0x0000000000000000000000000000000000000000: 0 wei + 1048576 gas x 10000000000 wei" + ); + }, + }); + + it({ + id: "T02", + title: "should appear in the txpool content", + test: async function () { + const content = (await context + .viem() + .transport.request({ method: "txpool_content" })) as any; + // web3 rpc returns lowercase + const data = content.queued[alith.address.toLowerCase()][toHex(1)]; + expect(data).toMatchObject({ + blockHash: null, + blockNumber: null, + from: alith.address.toLowerCase(), + gas: "0x100000", + gasPrice: "0x2540be400", + hash: txHash, + input: deployData, + nonce: toHex(1), + to: null, + transactionIndex: null, + value: "0x0", + }); + }, + }); + }, +}); diff --git a/test/moonwall/suites/dev/stagenet/txpool/test-txpool-pending.ts b/test/moonwall/suites/dev/stagenet/txpool/test-txpool-pending.ts new file mode 100644 index 00000000..c2f86188 --- /dev/null +++ b/test/moonwall/suites/dev/stagenet/txpool/test-txpool-pending.ts @@ -0,0 +1,84 @@ +import { beforeAll, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli"; +import { ALITH_ADDRESS, createEthersTransaction, sendRawTransaction } from "@moonwall/util"; +import { encodeDeployData, toHex } from "viem"; + +describeSuite({ + id: "D023807", + title: "TxPool - Pending Ethereum transaction", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let txHash: string; + + beforeAll(async () => { + const { abi, bytecode } = fetchCompiledContract("MultiplyBy7"); + const deployData = encodeDeployData({ + abi, + bytecode, + }); + + const rawTxn = await createEthersTransaction(context, { + data: deployData, + gasLimit: 1048576, + nonce: 0, + }); + txHash = await sendRawTransaction(context, rawTxn); + }); + + it({ + id: "T01", + title: "should appear in the txpool inspection", + test: async function () { + const inspect = (await context + .viem() + .transport.request({ method: "txpool_inspect" })) as any; + + // web3 rpc returns lowercase + const data = inspect.pending[ALITH_ADDRESS.toLowerCase()][toHex(0)]; + expect(data).to.not.be.undefined; + expect(data).to.be.equal( + "0x0000000000000000000000000000000000000000: 0 wei + 1048576 gas x 10000000000 wei" + ); + }, + }); + + it({ + id: "T02", + title: "should be marked as pending", + test: async function () { + const pendingTransaction = await context + .viem() + .getTransaction({ hash: txHash as `0x${string}` }); + + // pending transactions do not know yet to which block they belong to + expect(pendingTransaction).to.include({ + blockNumber: null, + hash: txHash, + }); + }, + }); + + it({ + id: "T03", + title: "should appear in the txpool content", + test: async function () { + const content = (await context + .viem() + .transport.request({ method: "txpool_content" })) as any; + + // web3 rpc returns lowercase + const data = content.pending[ALITH_ADDRESS.toLowerCase()][toHex(0)]; + expect(data).to.include({ + blockHash: null, + blockNumber: null, + from: ALITH_ADDRESS.toLowerCase(), + gas: "0x100000", + gasPrice: "0x2540be400", + hash: txHash, + nonce: toHex(0), + to: null, + value: "0x0", + }); + }, + }); + }, +});