From a4bd62582867c8c86c7ceb0f9192646c4be0f7ce Mon Sep 17 00:00:00 2001 From: Petr Ermishkin Date: Mon, 30 Dec 2024 13:00:57 +0300 Subject: [PATCH] E2E-test: `withdrawAll` from the contracts' factory between two CREATEs --- .gitignore | 1 + .../e2e-tests/ts/lib/abis/contractsFactory.ts | 75 +++++++++++++++ .../ts/tests/eth/contractsFactory.ts | 96 +++++++++++++++++++ utils/e2e-tests/ts/vitest.config.ts | 2 +- 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 utils/e2e-tests/ts/lib/abis/contractsFactory.ts create mode 100644 utils/e2e-tests/ts/tests/eth/contractsFactory.ts diff --git a/.gitignore b/.gitignore index df88794b6..e5352dfda 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ !/.yarn/releases !/.yarn/sdks !/.yarn/versions +/utils/e2e-tests/ts/node_modules diff --git a/utils/e2e-tests/ts/lib/abis/contractsFactory.ts b/utils/e2e-tests/ts/lib/abis/contractsFactory.ts new file mode 100644 index 000000000..fd5e7d61f --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/contractsFactory.ts @@ -0,0 +1,75 @@ +// pragma solidity ^0.8.13; +// +// contract ContractsFactory { +// address private owner; +// +// /** +// * Sets contract deployer as owner. +// */ +// constructor() { +// owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor +// } +// +// function build() +// public +// returns (Item itemAddress) +// { +// return new Item(); +// } +// +// function deposit() external payable {} +// +// function withdrawAll() external isOwner { +// (bool success,) = owner.call{value:address(this).balance}(""); +// require(success, "Transfer failed!"); +// } +// +// /** +// * Makes sure if caller is owner. +// */ +// modifier isOwner() { +// require(msg.sender == owner, "Caller is not owner"); +// _; +// } +// } +// +// contract Item {} + +export default { + abi: [ + { + type: "constructor", + inputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "build", + inputs: [], + outputs: [ + { + name: "itemAddress", + type: "address", + internalType: "contract Item", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "deposit", + inputs: [], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "withdrawAll", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b505f80546001600160a01b0319163317905561022d8061002d5f395ff3fe608060405260043610610033575f3560e01c8063853828b6146100375780638e1a55fc1461004d578063d0e30db01461004b575b5f5ffd5b348015610042575f5ffd5b5061004b61007d565b005b348015610058575f5ffd5b50610061610167565b6040516001600160a01b03909116815260200160405180910390f35b5f546001600160a01b031633146100d15760405162461bcd60e51b815260206004820152601360248201527221b0b63632b91034b9903737ba1037bbb732b960691b60448201526064015b60405180910390fd5b5f80546040516001600160a01b039091169047908381818185875af1925050503d805f811461011b576040519150601f19603f3d011682016040523d82523d5f602084013e610120565b606091505b50509050806101645760405162461bcd60e51b815260206004820152601060248201526f5472616e73666572206661696c65642160801b60448201526064016100c8565b50565b5f60405161017490610193565b604051809103905ff08015801561018d573d5f5f3e3d5ffd5b50905090565b6058806101a08339019056fe6080604052348015600e575f5ffd5b50603e80601a5f395ff3fe60806040525f5ffdfea2646970667358221220d3bdf3d2bfc6fbce1d0d2d42bf9323942c2a6cd1a0e747494d15f535449b35db64736f6c634300081c0033a26469706673582212202a42ba32ac9354cd2dbd675ca2ba09c51d4b8fdcc3ef5bd015da91685e8c113b64736f6c634300081c0033", +} as const; diff --git a/utils/e2e-tests/ts/tests/eth/contractsFactory.ts b/utils/e2e-tests/ts/tests/eth/contractsFactory.ts new file mode 100644 index 000000000..cfcd2144d --- /dev/null +++ b/utils/e2e-tests/ts/tests/eth/contractsFactory.ts @@ -0,0 +1,96 @@ +import { expect, describe, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import contractsFactory from "../../lib/abis/contractsFactory"; +import "../../lib/expect"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; + +describe("contracts factory", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode({ args: ["--dev", "--tmp"] }, cleanup.push); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + it("creates contracts at unique addresses", async () => { + const [alice, bob] = devClients; + + const deployContractTxHash = await alice.deployContract({ + abi: contractsFactory.abi, + bytecode: contractsFactory.bytecode, + }); + const deployContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployContractTxHash, + timeout: 18_000, + }); + const factoryAddress = deployContractTxReceipt.contractAddress!; + + const depositPromise = alice + .writeContract({ + address: factoryAddress, + abi: contractsFactory.abi, + functionName: "deposit", + value: 1n, // Even the smallest deposit is enough + }) + .then((depositTx) => + publicClient.waitForTransactionReceipt({ + hash: depositTx, + timeout: 18_000, + }), + ); + const item1AddressPromise = build(factoryAddress, bob, publicClient); + // Trying to wait for responses in parallel is worth it, since the test is already too long. + // Ordering between `deposit` and 1st `build` doesn't matter. + // Contract factory's `CREATE` nonce for the 1st `build` will be 1. + const [item1Address] = await Promise.all([ + item1AddressPromise, + depositPromise, + ]); + + // If there's a bug in the EVM, it will clear the contract state after `withdrawAll`. + const withdrawalTx = await alice.writeContract({ + address: factoryAddress, + abi: contractsFactory.abi, + functionName: "withdrawAll", + }); + await publicClient.waitForTransactionReceipt({ + hash: withdrawalTx, + timeout: 18_000, + }); + + // Contract factory's `CREATE` nonce for the 2nd `build` should be 2 (in the buggy EVM: 0). + await build(factoryAddress, bob, publicClient); + + // Contract factory's `CREATE` nonce for the 3rd `build` should be 3. + // In the buggy EVM nonce = 1, the same as for the 1st `build`; transaction will be reverted. + const item3Address = await build(factoryAddress, bob, publicClient); + expect(item1Address).not.toBe(item3Address); + }); +}); + +async function build( + factoryAddress: `0x${string}`, + client: eth.DevClientsWebSocket[0], + publicClient: eth.PublicClientWebSocket, +): Promise<`0x${string}`> { + const { request: buildingRequest, result: itemAddress } = + await publicClient.simulateContract({ + address: factoryAddress, + abi: contractsFactory.abi, + functionName: "build", + account: client.account, + }); + const buildingTx = await client.writeContract(buildingRequest); + await publicClient.waitForTransactionReceipt({ + hash: buildingTx, + timeout: 18_000, + }); + return itemAddress; +} diff --git a/utils/e2e-tests/ts/vitest.config.ts b/utils/e2e-tests/ts/vitest.config.ts index 40e6eb4c6..75faf2bf1 100644 --- a/utils/e2e-tests/ts/vitest.config.ts +++ b/utils/e2e-tests/ts/vitest.config.ts @@ -18,6 +18,6 @@ export default defineConfig({ singleThread: true, }, }, - testTimeout: 30_000, + testTimeout: 40_000, }, });