From dd9b48907f699d5a0196e436f731443fa12b0ff7 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Tue, 8 Oct 2024 09:49:32 -0700 Subject: [PATCH 01/11] refactor: Initial restructure of ABI basic function tests --- tests/e2e-evm/test/abi_basic.test.ts | 418 +++++++++++++++++---------- 1 file changed, 261 insertions(+), 157 deletions(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index 02094fc06..551039a4a 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -7,8 +7,17 @@ import type { GetContractReturnType, } from "@nomicfoundation/hardhat-viem/types"; import { expect } from "chai"; -import { Address, Hex, toFunctionSelector, toFunctionSignature, concat, encodeFunctionData } from "viem"; -import { Abi } from "abitype"; +import { + Address, + Hex, + toFunctionSelector, + toFunctionSignature, + concat, + encodeFunctionData, + CallParameters, + Chain, +} from "viem"; +import { Abi, AbiFallback, AbiFunction, AbiReceive } from "abitype"; import { getAbiFallbackFunction, getAbiReceiveFunction } from "./helpers/abi"; import { whaleAddress } from "./addresses"; @@ -124,6 +133,223 @@ describe("ABI_BasicTests", function () { address: Address; caller: Address; } + + // ShouldRunFn is a function that determines if a test case should be run on a + // specific function. This is useful if you only want to run a test case on + // specific functions. + type ShouldRunFn = ( + fn: AbiFunction, + receiveFunction: AbiReceive | undefined, + fallbackFunction: AbiFallback | undefined, + ) => boolean; + + // AbiFunctionTestCase defines a test case for an ABI function, this is run + // on EVERY function defined in an expected ABI. Use shouldRun to only run the + // test case on matching functions. + interface AbiFunctionTestCase { + name: string; + // If defined, only run this test case on functions that return true + shouldRun?: ShouldRunFn; + + txParams: (ctx: AbiContext, funcSelector: `0x${string}`) => CallParameters; + expectedStatus: "success" | "reverted"; + // If defined, check the balance of the contract after the transaction + expectedBalance?: (startingBalance: bigint) => bigint; + } + + const AbiFunctionTestCases: AbiFunctionTestCase[] = [ + { + name: "can be called", + txParams: (ctx, funcSelector) => ({ to: ctx.address, data: funcSelector, gas: defaultGas }), + expectedStatus: "success", + }, + { + name: "can be called by low level contract call", + txParams: (ctx, funcSelector) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by static call", + shouldRun(fn) { + return fn.stateMutability === "view" || fn.stateMutability === "pure"; + }, + txParams: (ctx, funcSelector) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by static call with extra data", + shouldRun(fn) { + return fn.stateMutability === "view" || fn.stateMutability === "pure"; + }, + txParams: (ctx, funcSelector) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, concat([funcSelector, "0x01"])], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by high level contract call", + txParams: (ctx, funcSelector) => ({ + to: ctx.caller, + data: funcSelector, + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with value", + shouldRun(fn) { + return fn.stateMutability === "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: ctx.address, + data: funcSelector, + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not be called with value", + shouldRun(fn) { + return fn.stateMutability !== "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: ctx.address, + data: funcSelector, + gas: defaultGas, + value: 1n, + }), + // Balance stays the same + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can be called by low level contract call with value", + shouldRun(fn) { + return fn.stateMutability === "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: ctx.caller, + data: funcSelector, + gas: 50000n, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not be called by low level contract call with value", + shouldRun(fn) { + return fn.stateMutability !== "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: ctx.caller, + data: funcSelector, + gas: 50000n, + value: 1n, + }), + // Same + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can be called by high level contract call with value", + shouldRun(fn) { + return fn.stateMutability === "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not be called by high level contract call with value", + shouldRun(fn) { + return fn.stateMutability !== "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can be called with extra data", + txParams: (ctx, funcSelector) => ({ + to: ctx.address, + data: concat([funcSelector, "0x01"]), + gas: defaultGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with value and extra data", + shouldRun(fn) { + return fn.stateMutability === "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: ctx.address, + data: concat([funcSelector, "0x01"]), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not be called with value and extra data", + shouldRun(fn) { + return fn.stateMutability !== "payable"; + }, + txParams: (ctx, funcSelector) => ({ + to: ctx.address, + data: concat([funcSelector, "0x01"]), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + ]; + function itImplementsTheAbi(abi: Abi, getContext: () => Promise) { let ctx: AbiContext; const receiveFunction = getAbiReceiveFunction(abi); @@ -142,163 +368,41 @@ describe("ABI_BasicTests", function () { const funcSelector = toFunctionSelector(toFunctionSignature(funcDesc)); describe(`${funcDesc.name} ${funcDesc.stateMutability}`, function () { - const isPayable = funcDesc.stateMutability === "payable"; - - it("can be called", async function () { - const txData = { to: ctx.address, data: funcSelector, gas: defaultGas }; - - await expect(publicClient.call(txData)).to.be.fulfilled; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it("can be called by low level contract call", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, funcSelector], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; + // Run test cases for each function + for (const testCase of AbiFunctionTestCases) { + // Check if we should run this test case + if (testCase.shouldRun && !testCase.shouldRun(funcDesc, receiveFunction, fallbackFunction)) { + continue; + } - await expect(publicClient.call(txData)).to.be.fulfilled; + it(testCase.name, async function () { + const startingBalance = await publicClient.getBalance({ address: ctx.address }); - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); + const txData = testCase.txParams(ctx, funcSelector); - if (funcDesc.stateMutability === "view" || funcDesc.stateMutability === "pure") { - it("can be called by static call", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, funcSelector], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - await expect(publicClient.call(txData)).to.be.fulfilled; + // Only attempt to call if we expect it to succeed + if (testCase.expectedStatus === "success") { + await expect(publicClient.call(txData)).to.be.fulfilled; + } else { + await expect(publicClient.call(txData)).to.be.rejected; + } const txHash = await walletClient.sendTransaction(txData); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); + expect(txReceipt.status).to.equal(testCase.expectedStatus); - it("can be called by static call with extra data", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, concat([funcSelector, "0x01"])], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; + if (txData.gas) { + expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; + } - await expect(publicClient.call(txData)).to.be.fulfilled; + if (testCase.expectedBalance) { + const expectedBalance = testCase.expectedBalance(startingBalance); - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; + const balance = await publicClient.getBalance({ address: ctx.address }); + expect(balance).to.equal(expectedBalance); + } }); } - - it("can be called by high level contract call", async function () { - const txData = { to: ctx.caller, data: funcSelector, gas: contractCallerGas }; - - await expect(publicClient.call(txData)).to.be.fulfilled; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${isPayable ? "" : "not "}be called with value`, async function () { - const txData = { to: ctx.address, data: funcSelector, gas: defaultGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(isPayable ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - if (isPayable) { - expectedBalance = startingBalance + txData.value; - } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it(`can ${isPayable ? "" : "not "}be called by low level contract call with value`, async function () { - const txData = { to: ctx.caller, data: funcSelector, gas: 50000n, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(isPayable ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - if (isPayable) { - expectedBalance = startingBalance + txData.value; - } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it(`can ${isPayable ? "" : "not "}be called by high level contract call with value`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, funcSelector], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n}; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(isPayable ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - if (isPayable) { - expectedBalance = startingBalance + txData.value; - } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it("can be called with extra data", async function () { - const data = concat([funcSelector, "0x01"]); - const txData = { to: ctx.address, data: data, gas: defaultGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${isPayable ? "" : "not "}be called with value and extra data`, async function () { - const data = concat([funcSelector, "0x01"]); - const txData = { to: ctx.address, data: data, gas: defaultGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(isPayable ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - if (isPayable) { - expectedBalance = startingBalance + txData.value; - } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); }); } }); @@ -320,7 +424,7 @@ describe("ABI_BasicTests", function () { expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; }); - it("can be called by another contract with no data", async function() { + it("can be called by another contract with no data", async function () { const data = encodeFunctionData({ abi: caller.abi, functionName: "functionCall", @@ -413,7 +517,7 @@ describe("ABI_BasicTests", function () { functionName: "functionCall", args: [ctx.address, "0x"], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n}; + const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; const startingBalance = await publicClient.getBalance({ address: ctx.address }); const txHash = await walletClient.sendTransaction(txData); @@ -448,7 +552,7 @@ describe("ABI_BasicTests", function () { functionName: "functionCall", args: [ctx.address, "0x"], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n}; + const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; const startingBalance = await publicClient.getBalance({ address: ctx.address }); const txHash = await walletClient.sendTransaction(txData); @@ -478,7 +582,7 @@ describe("ABI_BasicTests", function () { functionName: "functionCall", args: [ctx.address, toFunctionSelector("does_not_exist()")], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas}; + const txData = { to: caller.address, data: data, gas: contractCallerGas }; const txHash = await walletClient.sendTransaction(txData); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); @@ -492,7 +596,7 @@ describe("ABI_BasicTests", function () { functionName: "functionStaticCall", args: [ctx.address, toFunctionSelector("does_not_exist()")], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas}; + const txData = { to: caller.address, data: data, gas: contractCallerGas }; const txHash = await walletClient.sendTransaction(txData); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); @@ -516,7 +620,7 @@ describe("ABI_BasicTests", function () { functionName: "functionCall", args: [ctx.address, "0x010203"], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas}; + const txData = { to: caller.address, data: data, gas: contractCallerGas }; const txHash = await walletClient.sendTransaction(txData); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); @@ -530,7 +634,7 @@ describe("ABI_BasicTests", function () { functionName: "functionStaticCall", args: [ctx.address, "0x010203"], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas}; + const txData = { to: caller.address, data: data, gas: contractCallerGas }; const txHash = await walletClient.sendTransaction(txData); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); @@ -563,7 +667,7 @@ describe("ABI_BasicTests", function () { functionName: "functionCall", args: [ctx.address, toFunctionSelector("does_not_exist()")], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n}; + const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; const startingBalance = await publicClient.getBalance({ address: ctx.address }); const txHash = await walletClient.sendTransaction(txData); @@ -603,7 +707,7 @@ describe("ABI_BasicTests", function () { functionName: "functionCall", args: [ctx.address, "0x010203"], }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n}; + const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; const startingBalance = await publicClient.getBalance({ address: ctx.address }); const txHash = await walletClient.sendTransaction(txData); From 5737b3b8fbb48a4cb06a1a6b8c0370c4bbb04d79 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Tue, 8 Oct 2024 11:03:01 -0700 Subject: [PATCH 02/11] refactor: Restructure of ABI fallback tests --- tests/e2e-evm/test/abi_basic.test.ts | 798 +++++++++++++++++---------- 1 file changed, 496 insertions(+), 302 deletions(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index 551039a4a..5f97b1123 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -143,21 +143,32 @@ describe("ABI_BasicTests", function () { fallbackFunction: AbiFallback | undefined, ) => boolean; - // AbiFunctionTestCase defines a test case for an ABI function, this is run + // AbiFunctionTestCaseBase defines a test case for an ABI function, this is run // on EVERY function defined in an expected ABI. Use shouldRun to only run the // test case on matching functions. - interface AbiFunctionTestCase { + interface AbiFunctionTestCaseBase { name: string; // If defined, only run this test case on functions that return true shouldRun?: ShouldRunFn; - - txParams: (ctx: AbiContext, funcSelector: `0x${string}`) => CallParameters; expectedStatus: "success" | "reverted"; // If defined, check the balance of the contract after the transaction expectedBalance?: (startingBalance: bigint) => bigint; } - const AbiFunctionTestCases: AbiFunctionTestCase[] = [ + // AbiFunctionTestCase is a test case for a specific function in an ABI that + // includes the function selector. + type AbiFunctionTestCase = AbiFunctionTestCaseBase & { + txParams: (ctx: AbiContext, funcSelector: `0x${string}`) => CallParameters; + }; + + // AbiFallbackTestCase is a test case for the fallback function, which does + // not use a function selector. + type AbiFallbackTestCase = AbiFunctionTestCaseBase & { + // No function selector for fallback + txParams: (ctx: AbiContext) => CallParameters; + }; + + const abiFunctionTestCases: AbiFunctionTestCase[] = [ { name: "can be called", txParams: (ctx, funcSelector) => ({ to: ctx.address, data: funcSelector, gas: defaultGas }), @@ -350,6 +361,465 @@ describe("ABI_BasicTests", function () { }, ]; + // Test cases for special functions, receive and fallback + const specialFunctionTests: AbiFallbackTestCase[] = [ + // Has receive function OR payable fallback + { + name: "can receive zero value transfers with no data", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!receiveFunction || !!fallbackFunction; + }, + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), + expectedStatus: "success", + }, + { + name: "can be called by another contract with no data", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!receiveFunction || !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by static call with no data", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!receiveFunction || !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + + // No receive function AND no payable fallback + { + name: "can not receive zero value transfers with no data", + shouldRun(_, receiveFunction, fallbackFunction) { + return !receiveFunction && !fallbackFunction; + }, + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), + expectedStatus: "reverted", + }, + { + name: "can not receive zero value transfers by high level contract call with no data", + shouldRun(_, receiveFunction, fallbackFunction) { + return !receiveFunction && !fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not receive zero value transfers by static call with no data", + shouldRun(_, receiveFunction, fallbackFunction) { + return !receiveFunction && !fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + + // No receive function AND no payable fallback + { + name: "can not receive plain transfers", + shouldRun(_, receiveFunction, fallbackFunction) { + // No receive function and no payable fallback + return !receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable"); + }, + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), + expectedStatus: "reverted", + }, + { + name: "can not receive plain transfers via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + // No receive function and no payable fallback + return !receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable"); + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedStatus: "reverted", + }, + // Has receive function OR payable fallback + { + name: "can receive plain transfers", + shouldRun(_, receiveFunction, fallbackFunction) { + // Has receive function OR payable fallback + return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); + }, + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), + expectedStatus: "success", + }, + { + name: "can receive plain transfers via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + // Has receive function OR payable fallback + return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedStatus: "success", + }, + + { + name: "can be called with a non-matching function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + }), + expectedStatus: "success", + }, + { + name: "can not be called with a non-matching function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !fallbackFunction; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + }), + expectedStatus: "reverted", + }, + + { + name: "can be called with a non-matching function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can not be called with a non-matching function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + + { + name: "can be called with a non-matching function selector via static call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can not be called with a non-matching function selector via static call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + + { + name: "can be called with an invalid (short) function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + }), + expectedStatus: "success", + }, + { + name: "can not be called with an invalid (short) function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !fallbackFunction; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + }), + expectedStatus: "reverted", + }, + + { + name: "can be called with an invalid (short) function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can not be called with an invalid (short) function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + + { + name: "can be called with an invalid (short) function selector via static call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can not be called with an invalid (short) function selector via static call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !fallbackFunction; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + + // Fallback payable tests + { + name: "can receive value with a non-matching function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not receive value with a non-matching function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + + { + name: "can receive value with a non-matching function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not receive value with a non-matching function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + + { + name: "can receive value with an invalid (short) function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not receive value with an invalid (short) function selector", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; + }, + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + + { + name: "can receive value with an invalid (short) function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can not receive value with an invalid (short) function selector via message call", + shouldRun(_, receiveFunction, fallbackFunction) { + return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; + }, + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + ]; + function itImplementsTheAbi(abi: Abi, getContext: () => Promise) { let ctx: AbiContext; const receiveFunction = getAbiReceiveFunction(abi); @@ -369,7 +839,7 @@ describe("ABI_BasicTests", function () { describe(`${funcDesc.name} ${funcDesc.stateMutability}`, function () { // Run test cases for each function - for (const testCase of AbiFunctionTestCases) { + for (const testCase of abiFunctionTestCases) { // Check if we should run this test case if (testCase.shouldRun && !testCase.shouldRun(funcDesc, receiveFunction, fallbackFunction)) { continue; @@ -380,7 +850,6 @@ describe("ABI_BasicTests", function () { const txData = testCase.txParams(ctx, funcSelector); - // Only attempt to call if we expect it to succeed if (testCase.expectedStatus === "success") { await expect(publicClient.call(txData)).to.be.fulfilled; } else { @@ -400,6 +869,9 @@ describe("ABI_BasicTests", function () { const balance = await publicClient.getBalance({ address: ctx.address }); expect(balance).to.equal(expectedBalance); + } else { + const balance = await publicClient.getBalance({ address: ctx.address }); + expect(balance).to.equal(startingBalance, "balance to not change if expectedBalance is not defined"); } }); } @@ -414,313 +886,35 @@ describe("ABI_BasicTests", function () { // Fallback functions can be payable or non-payable and can receive data in both cases const testName = `ABI special functions: ${receiveFunction ? "" : "no "}receive and ${fallbackFunction ? fallbackFunction.stateMutability : "no"} fallback`; describe(testName, function () { - if (receiveFunction || fallbackFunction) { - it("can receive zero value transfers with no data", async function () { - const txData = { to: ctx.address, gas: defaultGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it("can be called by another contract with no data", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - await expect(publicClient.call(txData)).to.be.fulfilled; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it("can be called by static call with no data", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - await expect(publicClient.call(txData)).to.be.fulfilled; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - } - - if (!receiveFunction && !fallbackFunction) { - it("can not receive zero value transfers with no data", async function () { - const txData = { to: ctx.address, gas: defaultGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it("can not receive zero value transfers with no data", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it("can not be called by static call with no data", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - } - - if (!receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable")) { - it("can not receive plain transfers", async function () { - const txData = { to: ctx.address, gas: defaultGas, value: 1n }; + for (const testCase of specialFunctionTests) { + it(testCase.name, async function () { const startingBalance = await publicClient.getBalance({ address: ctx.address }); - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it("can not receive plain transfers via message call", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - } - - if (receiveFunction || (fallbackFunction && fallbackFunction.stateMutability === "payable")) { - it("can receive plain transfers", async function () { - const txData = { to: ctx.address, gas: defaultGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - const expectedBalance = startingBalance + txData.value; - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it("can plain transfers via message call", async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal("success"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - const expectedBalance = startingBalance + txData.value; - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - } - - it(`can ${fallbackFunction ? "" : "not "}be called with a non-matching function selector`, async function () { - const data = toFunctionSelector("does_not_exist()"); - const txData = { to: ctx.address, data: data, gas: defaultGas }; + const txData = testCase.txParams(ctx); - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${fallbackFunction ? "" : "not "}be called with a non-matching function selector via message call`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${fallbackFunction ? "" : "not "}be called with a non-matching function selector via static call`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${fallbackFunction ? "" : "not "}be called with an invalid (short) function selector`, async function () { - const data: Hex = "0x010203"; - const txData = { to: ctx.address, data: data, gas: defaultGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${fallbackFunction ? "" : "not "}be called with an invalid (short) via message call`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x010203"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - it(`can ${fallbackFunction ? "" : "not "}be called with an invalid (short) via static call`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x010203"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas }; - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - }); - - if (fallbackFunction) { - it(`can ${fallbackFunction.stateMutability === "payable" ? "" : "not "}receive value with a non-matching function selector`, async function () { - const data = toFunctionSelector("does_not_exist()"); - const txData = { to: ctx.address, data: data, gas: defaultGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction.stateMutability === "payable" ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - if (fallbackFunction.stateMutability === "payable") { - expectedBalance = startingBalance + txData.value; + if (testCase.expectedStatus === "success") { + await expect(publicClient.call(txData)).to.be.fulfilled; + } else { + await expect(publicClient.call(txData)).to.be.rejected; } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it(`can ${fallbackFunction.stateMutability === "payable" ? "" : "not "}receive value with a non-matching function selector via message call`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); const txHash = await walletClient.sendTransaction(txData); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction.stateMutability === "payable" ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; + expect(txReceipt.status).to.equal(testCase.expectedStatus); - let expectedBalance = startingBalance; - if (fallbackFunction.stateMutability === "payable") { - expectedBalance = startingBalance + txData.value; + if (txData.gas) { + expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it(`can ${fallbackFunction.stateMutability === "payable" ? "" : "not "}recieve value with an invalid function selector`, async function () { - const data: Hex = "0x010203"; - const txData = { to: ctx.address, data: data, gas: defaultGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction.stateMutability === "payable" ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; - - let expectedBalance = startingBalance; - if (fallbackFunction.stateMutability === "payable") { - expectedBalance = startingBalance + txData.value; - } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); - }); - - it(`can ${fallbackFunction.stateMutability === "payable" ? "" : "not "}recieve value with an invalid function selector via message call`, async function () { - const data = encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x010203"], - }); - const txData = { to: caller.address, data: data, gas: contractCallerGas, value: 1n }; - const startingBalance = await publicClient.getBalance({ address: ctx.address }); - - const txHash = await walletClient.sendTransaction(txData); - const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - expect(txReceipt.status).to.equal(fallbackFunction.stateMutability === "payable" ? "success" : "reverted"); - expect(txReceipt.gasUsed < txData.gas, "gas to not be exhausted").to.be.true; + if (testCase.expectedBalance) { + const expectedBalance = testCase.expectedBalance(startingBalance); - let expectedBalance = startingBalance; - if (fallbackFunction.stateMutability === "payable") { - expectedBalance = startingBalance + txData.value; + const balance = await publicClient.getBalance({ address: ctx.address }); + expect(balance).to.equal(expectedBalance); + } else { + const balance = await publicClient.getBalance({ address: ctx.address }); + expect(balance).to.equal(startingBalance, "balance to not change if expectedBalance is not defined"); } - const balance = await publicClient.getBalance({ address: ctx.address }); - expect(balance).to.equal(expectedBalance); }); } }); From b18490704b68f46d29521c660ea6960afb7eaa81 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Tue, 8 Oct 2024 13:59:23 -0700 Subject: [PATCH 03/11] fix: Use separate shouldRun fn signatures for fallback tests Fallback tests are not run on each function but rather only for the specific fallback. This means whether or not to run the test is determined by only the receive and fallback functions, without any other ABI function. --- tests/e2e-evm/test/abi_basic.test.ts | 95 +++++++++++++++------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index 5f97b1123..df24fbc91 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -134,22 +134,11 @@ describe("ABI_BasicTests", function () { caller: Address; } - // ShouldRunFn is a function that determines if a test case should be run on a - // specific function. This is useful if you only want to run a test case on - // specific functions. - type ShouldRunFn = ( - fn: AbiFunction, - receiveFunction: AbiReceive | undefined, - fallbackFunction: AbiFallback | undefined, - ) => boolean; - - // AbiFunctionTestCaseBase defines a test case for an ABI function, this is run - // on EVERY function defined in an expected ABI. Use shouldRun to only run the - // test case on matching functions. + // AbiFunctionTestCaseBase defines a test case for an ABI function, this is + // run on every function defined in an expected ABI. Use shouldRun to only run + // the test case on matching functions. interface AbiFunctionTestCaseBase { name: string; - // If defined, only run this test case on functions that return true - shouldRun?: ShouldRunFn; expectedStatus: "success" | "reverted"; // If defined, check the balance of the contract after the transaction expectedBalance?: (startingBalance: bigint) => bigint; @@ -158,16 +147,27 @@ describe("ABI_BasicTests", function () { // AbiFunctionTestCase is a test case for a specific function in an ABI that // includes the function selector. type AbiFunctionTestCase = AbiFunctionTestCaseBase & { + // If defined, only run this test case on functions that return true. This + // is useful for testing specific function types. + shouldRun?: ( + fn: AbiFunction, + receiveFunction: AbiReceive | undefined, + fallbackFunction: AbiFallback | undefined, + ) => boolean; txParams: (ctx: AbiContext, funcSelector: `0x${string}`) => CallParameters; }; // AbiFallbackTestCase is a test case for the fallback function, which does // not use a function selector. type AbiFallbackTestCase = AbiFunctionTestCaseBase & { + // Same as the shouldRun function for AbiFunctionTestCase, but without a + // specific ABIFunction. + shouldRun?: (receiveFunction: AbiReceive | undefined, fallbackFunction: AbiFallback | undefined) => boolean; // No function selector for fallback txParams: (ctx: AbiContext) => CallParameters; }; + // Define test cases for ABI function compliance const abiFunctionTestCases: AbiFunctionTestCase[] = [ { name: "can be called", @@ -361,12 +361,12 @@ describe("ABI_BasicTests", function () { }, ]; - // Test cases for special functions, receive and fallback + // Define test cases for special functions, receive and fallback const specialFunctionTests: AbiFallbackTestCase[] = [ // Has receive function OR payable fallback { name: "can receive zero value transfers with no data", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!receiveFunction || !!fallbackFunction; }, txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), @@ -374,7 +374,7 @@ describe("ABI_BasicTests", function () { }, { name: "can be called by another contract with no data", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!receiveFunction || !!fallbackFunction; }, txParams: (ctx) => ({ @@ -390,7 +390,7 @@ describe("ABI_BasicTests", function () { }, { name: "can be called by static call with no data", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!receiveFunction || !!fallbackFunction; }, txParams: (ctx) => ({ @@ -408,7 +408,7 @@ describe("ABI_BasicTests", function () { // No receive function AND no payable fallback { name: "can not receive zero value transfers with no data", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !receiveFunction && !fallbackFunction; }, txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), @@ -416,7 +416,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive zero value transfers by high level contract call with no data", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !receiveFunction && !fallbackFunction; }, txParams: (ctx) => ({ @@ -432,7 +432,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive zero value transfers by static call with no data", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !receiveFunction && !fallbackFunction; }, txParams: (ctx) => ({ @@ -450,7 +450,7 @@ describe("ABI_BasicTests", function () { // No receive function AND no payable fallback { name: "can not receive plain transfers", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { // No receive function and no payable fallback return !receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable"); }, @@ -459,7 +459,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive plain transfers via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { // No receive function and no payable fallback return !receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable"); }, @@ -478,7 +478,7 @@ describe("ABI_BasicTests", function () { // Has receive function OR payable fallback { name: "can receive plain transfers", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { // Has receive function OR payable fallback return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); }, @@ -487,7 +487,7 @@ describe("ABI_BasicTests", function () { }, { name: "can receive plain transfers via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { // Has receive function OR payable fallback return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); }, @@ -506,7 +506,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with a non-matching function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -518,7 +518,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with a non-matching function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -531,7 +531,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with a non-matching function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -547,7 +547,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with a non-matching function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -564,7 +564,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with a non-matching function selector via static call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -580,7 +580,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with a non-matching function selector via static call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -597,7 +597,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with an invalid (short) function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -609,7 +609,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with an invalid (short) function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -622,7 +622,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with an invalid (short) function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -638,7 +638,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with an invalid (short) function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -655,7 +655,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with an invalid (short) function selector via static call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -671,7 +671,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with an invalid (short) function selector via static call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -689,7 +689,7 @@ describe("ABI_BasicTests", function () { // Fallback payable tests { name: "can receive value with a non-matching function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -703,7 +703,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with a non-matching function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -718,7 +718,7 @@ describe("ABI_BasicTests", function () { { name: "can receive value with a non-matching function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -736,7 +736,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with a non-matching function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -755,7 +755,7 @@ describe("ABI_BasicTests", function () { { name: "can receive value with an invalid (short) function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -769,7 +769,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with an invalid (short) function selector", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -784,7 +784,7 @@ describe("ABI_BasicTests", function () { { name: "can receive value with an invalid (short) function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -802,7 +802,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with an invalid (short) function selector via message call", - shouldRun(_, receiveFunction, fallbackFunction) { + shouldRun(receiveFunction, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -887,6 +887,11 @@ describe("ABI_BasicTests", function () { const testName = `ABI special functions: ${receiveFunction ? "" : "no "}receive and ${fallbackFunction ? fallbackFunction.stateMutability : "no"} fallback`; describe(testName, function () { for (const testCase of specialFunctionTests) { + // Check if we should run this test case + if (testCase.shouldRun && !testCase.shouldRun(receiveFunction, fallbackFunction)) { + continue; + } + it(testCase.name, async function () { const startingBalance = await publicClient.getBalance({ address: ctx.address }); From 815415cbba85429feb1dd2ef7c4d3f4504623cc4 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Tue, 8 Oct 2024 16:13:54 -0700 Subject: [PATCH 04/11] fix: Resolve use of type assertions, enforcing types Previously used type assertions to bypass certain TypeScript issues with test cases, along with using any & unsafe assignments. This resolves the types to be properly valid and enforced to prevent any potential errors. --- tests/e2e-evm/test/abi_basic.test.ts | 58 ++++++++++++++++++---------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index df24fbc91..b23cbda31 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -1,11 +1,6 @@ import hre from "hardhat"; import type { ArtifactsMap } from "hardhat/types/artifacts"; -import type { - PublicClient, - WalletClient, - ContractName, - GetContractReturnType, -} from "@nomicfoundation/hardhat-viem/types"; +import type { PublicClient, WalletClient, GetContractReturnType } from "@nomicfoundation/hardhat-viem/types"; import { expect } from "chai"; import { Address, @@ -16,6 +11,7 @@ import { encodeFunctionData, CallParameters, Chain, + isHex, } from "viem"; import { Abi, AbiFallback, AbiFunction, AbiReceive } from "abitype"; import { getAbiFallbackFunction, getAbiReceiveFunction } from "./helpers/abi"; @@ -24,25 +20,27 @@ import { whaleAddress } from "./addresses"; const defaultGas = 25000n; const contractCallerGas = defaultGas + 10000n; -interface ContractTestCase { +interface ContractTestCase<> { interface: keyof ArtifactsMap; - mock: ContractName; + // Ensures contract name ends with "Mock", but does not enforce the prefix + // matches the interface. + mock: `${keyof ArtifactsMap}Mock`; precompile: Address; - caller: ContractName; + caller: keyof ArtifactsMap; } const precompiles: Address[] = [ - "0x9000000000000000000000000000000000000001", // noop no recieve no fallback + "0x9000000000000000000000000000000000000001", // noop no receive no fallback "0x9000000000000000000000000000000000000002", // noop receive no fallback "0x9000000000000000000000000000000000000003", // noop receive payable fallback "0x9000000000000000000000000000000000000004", // noop receive non payable fallback "0x9000000000000000000000000000000000000005", // noop no receive payable fallback - "0x9000000000000000000000000000000000000006", // noop no recieve non payable fallback + "0x9000000000000000000000000000000000000006", // noop no receive non payable fallback ]; // ABI_BasicTests assert ethereum + solidity transaction ABI interactions perform as expected. describe("ABI_BasicTests", function () { - const testCases = [ + const testCases: ContractTestCase[] = [ // Test function modifiers without receive & fallback { interface: "NoopNoReceiveNoFallback", @@ -84,7 +82,7 @@ describe("ABI_BasicTests", function () { precompile: precompiles[5], caller: "NoopCaller", }, - ] as ContractTestCase[]; + ]; // // Client + Wallet Setup @@ -483,6 +481,7 @@ describe("ABI_BasicTests", function () { return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); }, txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), + expectedBalance: (startingBalance) => startingBalance + 1n, expectedStatus: "success", }, { @@ -501,6 +500,7 @@ describe("ABI_BasicTests", function () { gas: contractCallerGas, value: 1n, }), + expectedBalance: (startingBalance) => startingBalance + 1n, expectedStatus: "success", }, @@ -935,6 +935,9 @@ describe("ABI_BasicTests", function () { describe(tc.interface, function () { const abi = hre.artifacts.readArtifactSync(tc.interface).abi; + // Enforce that the mock contract name starts with the interface name + expect(tc.mock.startsWith(tc.interface), "Mock contract name must start with the interface name").to.be.true; + // The interface is tested against a mock on all networks. // This serves as a reference to ensure all precompiles have mocks that behavior similarly // for testing on non-kava networks, in addition to ensure that we match normal contract @@ -945,11 +948,24 @@ describe("ABI_BasicTests", function () { let deployedBytecode: Hex; before("deploy mock", async function () { - mockAddress = (await hre.viem.deployContract(tc.mock)).address; - callerAddress = (await hre.viem.deployContract(tc.caller, [mockAddress])).address; - // TODO: fix typing and do not use explicit any, unsafe assignment, or unsafe access - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - deployedBytecode = ((await hre.artifacts.readArtifact(tc.mock)) as any).deployedBytecode; + // Make type keyof ArtifactsMap to be a string as workaround for + // viem.deployContract() not accepting keyof ArtifactsMap. Each + // contract has it's own deployContract overload that accepts a + // literal string for the contract name, but using keyof ArtifactsMap + // falls back to the generic overload that only accepts a string NOT + // in the ArtifactsMap. + const mockContractName: string = tc.mock; + const callerContractName: string = tc.caller; + + mockAddress = (await hre.viem.deployContract(mockContractName)).address; + callerAddress = (await hre.viem.deployContract(callerContractName, [mockAddress])).address; + + const mockArtifact = await hre.artifacts.readArtifact(mockContractName); + if (isHex(mockArtifact.deployedBytecode)) { + deployedBytecode = mockArtifact.deployedBytecode; + } else { + expect.fail("deployedBytecode is not hex"); + } }); itHasCorrectState(() => @@ -975,8 +991,10 @@ describe("ABI_BasicTests", function () { const precompileAddress = tc.precompile; let callerAddress: Address; - before("deploy pecompile caller", async function () { - callerAddress = (await hre.viem.deployContract(tc.caller, [precompileAddress])).address; + before("deploy precompile caller", async function () { + const callerContractName: string = tc.caller; + + callerAddress = (await hre.viem.deployContract(callerContractName, [precompileAddress])).address; }); itHasCorrectState(() => From 7ec0ffe4171f598bf9caea3200adb7416a56ba84 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Tue, 8 Oct 2024 16:20:38 -0700 Subject: [PATCH 05/11] refactor: Rename unused params in test cases --- tests/e2e-evm/test/abi_basic.test.ts | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index b23cbda31..988082f35 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -506,7 +506,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with a non-matching function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -518,7 +518,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with a non-matching function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -531,7 +531,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with a non-matching function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -547,7 +547,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with a non-matching function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -564,7 +564,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with a non-matching function selector via static call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -580,7 +580,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with a non-matching function selector via static call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -597,7 +597,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with an invalid (short) function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -609,7 +609,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with an invalid (short) function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -622,7 +622,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with an invalid (short) function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -638,7 +638,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with an invalid (short) function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -655,7 +655,7 @@ describe("ABI_BasicTests", function () { { name: "can be called with an invalid (short) function selector via static call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction; }, txParams: (ctx) => ({ @@ -671,7 +671,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not be called with an invalid (short) function selector via static call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !fallbackFunction; }, txParams: (ctx) => ({ @@ -689,7 +689,7 @@ describe("ABI_BasicTests", function () { // Fallback payable tests { name: "can receive value with a non-matching function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -703,7 +703,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with a non-matching function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -718,7 +718,7 @@ describe("ABI_BasicTests", function () { { name: "can receive value with a non-matching function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -736,7 +736,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with a non-matching function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -755,7 +755,7 @@ describe("ABI_BasicTests", function () { { name: "can receive value with an invalid (short) function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -769,7 +769,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with an invalid (short) function selector", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ @@ -784,7 +784,7 @@ describe("ABI_BasicTests", function () { { name: "can receive value with an invalid (short) function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; }, txParams: (ctx) => ({ @@ -802,7 +802,7 @@ describe("ABI_BasicTests", function () { }, { name: "can not receive value with an invalid (short) function selector via message call", - shouldRun(receiveFunction, fallbackFunction) { + shouldRun(_, fallbackFunction) { return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; }, txParams: (ctx) => ({ From 4df0c17f16b7538cb17d364dd6afb12b9607997a Mon Sep 17 00:00:00 2001 From: drklee3 Date: Wed, 9 Oct 2024 11:54:43 -0700 Subject: [PATCH 06/11] refactor: Dynamic generation basic ABI test cases Changes from including a field in each test case from conditionally running the case, to building the cases dynamically. This allows for logical grouping of test cases and organization with logic instead of using comments. Slightly less explicit for each test case, but with the grouping of test cases, it reduces the mental overhead of figuring out when each test case is run. --- tests/e2e-evm/test/abi_basic.test.ts | 1271 ++++++++++++-------------- 1 file changed, 577 insertions(+), 694 deletions(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index 988082f35..deaadc059 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -13,7 +13,7 @@ import { Chain, isHex, } from "viem"; -import { Abi, AbiFallback, AbiFunction, AbiReceive } from "abitype"; +import { Abi } from "abitype"; import { getAbiFallbackFunction, getAbiReceiveFunction } from "./helpers/abi"; import { whaleAddress } from "./addresses"; @@ -132,694 +132,16 @@ describe("ABI_BasicTests", function () { caller: Address; } - // AbiFunctionTestCaseBase defines a test case for an ABI function, this is - // run on every function defined in an expected ABI. Use shouldRun to only run - // the test case on matching functions. - interface AbiFunctionTestCaseBase { + // AbiFunctionTestCase defines a test case for an ABI function, this is + // run on every function defined in an expected ABI. + interface AbiFunctionTestCase { name: string; expectedStatus: "success" | "reverted"; + txParams: (ctx: AbiContext) => CallParameters; // If defined, check the balance of the contract after the transaction expectedBalance?: (startingBalance: bigint) => bigint; } - // AbiFunctionTestCase is a test case for a specific function in an ABI that - // includes the function selector. - type AbiFunctionTestCase = AbiFunctionTestCaseBase & { - // If defined, only run this test case on functions that return true. This - // is useful for testing specific function types. - shouldRun?: ( - fn: AbiFunction, - receiveFunction: AbiReceive | undefined, - fallbackFunction: AbiFallback | undefined, - ) => boolean; - txParams: (ctx: AbiContext, funcSelector: `0x${string}`) => CallParameters; - }; - - // AbiFallbackTestCase is a test case for the fallback function, which does - // not use a function selector. - type AbiFallbackTestCase = AbiFunctionTestCaseBase & { - // Same as the shouldRun function for AbiFunctionTestCase, but without a - // specific ABIFunction. - shouldRun?: (receiveFunction: AbiReceive | undefined, fallbackFunction: AbiFallback | undefined) => boolean; - // No function selector for fallback - txParams: (ctx: AbiContext) => CallParameters; - }; - - // Define test cases for ABI function compliance - const abiFunctionTestCases: AbiFunctionTestCase[] = [ - { - name: "can be called", - txParams: (ctx, funcSelector) => ({ to: ctx.address, data: funcSelector, gas: defaultGas }), - expectedStatus: "success", - }, - { - name: "can be called by low level contract call", - txParams: (ctx, funcSelector) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, funcSelector], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can be called by static call", - shouldRun(fn) { - return fn.stateMutability === "view" || fn.stateMutability === "pure"; - }, - txParams: (ctx, funcSelector) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, funcSelector], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can be called by static call with extra data", - shouldRun(fn) { - return fn.stateMutability === "view" || fn.stateMutability === "pure"; - }, - txParams: (ctx, funcSelector) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, concat([funcSelector, "0x01"])], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can be called by high level contract call", - txParams: (ctx, funcSelector) => ({ - to: ctx.caller, - data: funcSelector, - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can be called with value", - shouldRun(fn) { - return fn.stateMutability === "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: ctx.address, - data: funcSelector, - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not be called with value", - shouldRun(fn) { - return fn.stateMutability !== "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: ctx.address, - data: funcSelector, - gas: defaultGas, - value: 1n, - }), - // Balance stays the same - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - { - name: "can be called by low level contract call with value", - shouldRun(fn) { - return fn.stateMutability === "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: ctx.caller, - data: funcSelector, - gas: 50000n, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not be called by low level contract call with value", - shouldRun(fn) { - return fn.stateMutability !== "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: ctx.caller, - data: funcSelector, - gas: 50000n, - value: 1n, - }), - // Same - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - { - name: "can be called by high level contract call with value", - shouldRun(fn) { - return fn.stateMutability === "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, funcSelector], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not be called by high level contract call with value", - shouldRun(fn) { - return fn.stateMutability !== "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, funcSelector], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - { - name: "can be called with extra data", - txParams: (ctx, funcSelector) => ({ - to: ctx.address, - data: concat([funcSelector, "0x01"]), - gas: defaultGas, - }), - expectedStatus: "success", - }, - { - name: "can be called with value and extra data", - shouldRun(fn) { - return fn.stateMutability === "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: ctx.address, - data: concat([funcSelector, "0x01"]), - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not be called with value and extra data", - shouldRun(fn) { - return fn.stateMutability !== "payable"; - }, - txParams: (ctx, funcSelector) => ({ - to: ctx.address, - data: concat([funcSelector, "0x01"]), - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - ]; - - // Define test cases for special functions, receive and fallback - const specialFunctionTests: AbiFallbackTestCase[] = [ - // Has receive function OR payable fallback - { - name: "can receive zero value transfers with no data", - shouldRun(receiveFunction, fallbackFunction) { - return !!receiveFunction || !!fallbackFunction; - }, - txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), - expectedStatus: "success", - }, - { - name: "can be called by another contract with no data", - shouldRun(receiveFunction, fallbackFunction) { - return !!receiveFunction || !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can be called by static call with no data", - shouldRun(receiveFunction, fallbackFunction) { - return !!receiveFunction || !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x"], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - - // No receive function AND no payable fallback - { - name: "can not receive zero value transfers with no data", - shouldRun(receiveFunction, fallbackFunction) { - return !receiveFunction && !fallbackFunction; - }, - txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), - expectedStatus: "reverted", - }, - { - name: "can not receive zero value transfers by high level contract call with no data", - shouldRun(receiveFunction, fallbackFunction) { - return !receiveFunction && !fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }), - gas: contractCallerGas, - }), - expectedStatus: "reverted", - }, - { - name: "can not receive zero value transfers by static call with no data", - shouldRun(receiveFunction, fallbackFunction) { - return !receiveFunction && !fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x"], - }), - gas: contractCallerGas, - }), - expectedStatus: "reverted", - }, - - // No receive function AND no payable fallback - { - name: "can not receive plain transfers", - shouldRun(receiveFunction, fallbackFunction) { - // No receive function and no payable fallback - return !receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable"); - }, - txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), - expectedStatus: "reverted", - }, - { - name: "can not receive plain transfers via message call", - shouldRun(receiveFunction, fallbackFunction) { - // No receive function and no payable fallback - return !receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable"); - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedStatus: "reverted", - }, - // Has receive function OR payable fallback - { - name: "can receive plain transfers", - shouldRun(receiveFunction, fallbackFunction) { - // Has receive function OR payable fallback - return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); - }, - txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can receive plain transfers via message call", - shouldRun(receiveFunction, fallbackFunction) { - // Has receive function OR payable fallback - return !!receiveFunction || (!!fallbackFunction && fallbackFunction.stateMutability === "payable"); - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x"], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - - { - name: "can be called with a non-matching function selector", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: toFunctionSelector("does_not_exist()"), - gas: defaultGas, - }), - expectedStatus: "success", - }, - { - name: "can not be called with a non-matching function selector", - shouldRun(_, fallbackFunction) { - return !fallbackFunction; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: toFunctionSelector("does_not_exist()"), - gas: defaultGas, - }), - expectedStatus: "reverted", - }, - - { - name: "can be called with a non-matching function selector via message call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can not be called with a non-matching function selector via message call", - shouldRun(_, fallbackFunction) { - return !fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }), - gas: contractCallerGas, - }), - expectedStatus: "reverted", - }, - - { - name: "can be called with a non-matching function selector via static call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can not be called with a non-matching function selector via static call", - shouldRun(_, fallbackFunction) { - return !fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }), - gas: contractCallerGas, - }), - expectedStatus: "reverted", - }, - - { - name: "can be called with an invalid (short) function selector", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: "0x010203", - gas: defaultGas, - }), - expectedStatus: "success", - }, - { - name: "can not be called with an invalid (short) function selector", - shouldRun(_, fallbackFunction) { - return !fallbackFunction; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: "0x010203", - gas: defaultGas, - }), - expectedStatus: "reverted", - }, - - { - name: "can be called with an invalid (short) function selector via message call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x010203"], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can not be called with an invalid (short) function selector via message call", - shouldRun(_, fallbackFunction) { - return !fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x010203"], - }), - gas: contractCallerGas, - }), - expectedStatus: "reverted", - }, - - { - name: "can be called with an invalid (short) function selector via static call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x010203"], - }), - gas: contractCallerGas, - }), - expectedStatus: "success", - }, - { - name: "can not be called with an invalid (short) function selector via static call", - shouldRun(_, fallbackFunction) { - return !fallbackFunction; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionStaticCall", - args: [ctx.address, "0x010203"], - }), - gas: contractCallerGas, - }), - expectedStatus: "reverted", - }, - - // Fallback payable tests - { - name: "can receive value with a non-matching function selector", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: toFunctionSelector("does_not_exist()"), - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not receive value with a non-matching function selector", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: toFunctionSelector("does_not_exist()"), - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - - { - name: "can receive value with a non-matching function selector via message call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not receive value with a non-matching function selector via message call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, toFunctionSelector("does_not_exist()")], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - - { - name: "can receive value with an invalid (short) function selector", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: "0x010203", - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not receive value with an invalid (short) function selector", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; - }, - txParams: (ctx) => ({ - to: ctx.address, - data: "0x010203", - gas: defaultGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - - { - name: "can receive value with an invalid (short) function selector via message call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability === "payable"; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x010203"], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance + 1n, - expectedStatus: "success", - }, - { - name: "can not receive value with an invalid (short) function selector via message call", - shouldRun(_, fallbackFunction) { - return !!fallbackFunction && fallbackFunction.stateMutability !== "payable"; - }, - txParams: (ctx) => ({ - to: caller.address, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "functionCall", - args: [ctx.address, "0x010203"], - }), - gas: contractCallerGas, - value: 1n, - }), - expectedBalance: (startingBalance) => startingBalance, - expectedStatus: "reverted", - }, - ]; - function itImplementsTheAbi(abi: Abi, getContext: () => Promise) { let ctx: AbiContext; const receiveFunction = getAbiReceiveFunction(abi); @@ -837,18 +159,193 @@ describe("ABI_BasicTests", function () { const funcSelector = toFunctionSelector(toFunctionSignature(funcDesc)); + // Dynamically build test cases for each function individually, as + // the cases depend on the function's stateMutability. + const abiFunctionTestCases: AbiFunctionTestCase[] = [ + { + name: "can be called", + txParams: (ctx) => ({ to: ctx.address, data: funcSelector, gas: defaultGas }), + expectedStatus: "success", + }, + { + name: "can be called by low level contract call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by high level contract call", + txParams: (ctx) => ({ + to: ctx.caller, + data: funcSelector, + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with extra data", + txParams: (ctx) => ({ + to: ctx.address, + data: concat([funcSelector, "0x01"]), + gas: defaultGas, + }), + expectedStatus: "success", + }, + ]; + + if (funcDesc.stateMutability === "view" || funcDesc.stateMutability === "pure") { + abiFunctionTestCases.push( + { + name: "can be called by static call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by static call with extra data", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, concat([funcSelector, "0x01"])], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + ); + } + + if (funcDesc.stateMutability === "payable") { + abiFunctionTestCases.push( + { + name: "can be called with value", + txParams: (ctx) => ({ + to: ctx.address, + data: funcSelector, + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can be called by low level contract call with value", + txParams: (ctx) => ({ + to: ctx.caller, + data: funcSelector, + gas: 50000n, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can be called by high level contract call with value", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can be called with value and extra data", + txParams: (ctx) => ({ + to: ctx.address, + data: concat([funcSelector, "0x01"]), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + ); + } + + if (funcDesc.stateMutability !== "payable") { + abiFunctionTestCases.push( + { + name: "can not be called with value", + txParams: (ctx) => ({ + to: ctx.address, + data: funcSelector, + gas: defaultGas, + value: 1n, + }), + // Balance stays the same + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can not be called by low level contract call with value", + txParams: (ctx) => ({ + to: ctx.caller, + data: funcSelector, + gas: 50000n, + value: 1n, + }), + // Same + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can not be called by high level contract call with value", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, funcSelector], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can not be called with value and extra data", + txParams: (ctx) => ({ + to: ctx.address, + data: concat([funcSelector, "0x01"]), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + ); + } + describe(`${funcDesc.name} ${funcDesc.stateMutability}`, function () { // Run test cases for each function for (const testCase of abiFunctionTestCases) { - // Check if we should run this test case - if (testCase.shouldRun && !testCase.shouldRun(funcDesc, receiveFunction, fallbackFunction)) { - continue; - } - it(testCase.name, async function () { const startingBalance = await publicClient.getBalance({ address: ctx.address }); - const txData = testCase.txParams(ctx, funcSelector); + const txData = testCase.txParams(ctx); if (testCase.expectedStatus === "success") { await expect(publicClient.call(txData)).to.be.fulfilled; @@ -886,12 +383,398 @@ describe("ABI_BasicTests", function () { // Fallback functions can be payable or non-payable and can receive data in both cases const testName = `ABI special functions: ${receiveFunction ? "" : "no "}receive and ${fallbackFunction ? fallbackFunction.stateMutability : "no"} fallback`; describe(testName, function () { - for (const testCase of specialFunctionTests) { - // Check if we should run this test case - if (testCase.shouldRun && !testCase.shouldRun(receiveFunction, fallbackFunction)) { - continue; - } + // Dynamically build test cases depending on the receive and fallback + // functions defined in the ABI + const specialFunctionTests: AbiFunctionTestCase[] = []; + + if (receiveFunction && fallbackFunction) { + specialFunctionTests.push( + { + name: "can receive zero value transfers with no data", + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), + expectedStatus: "success", + }, + { + name: "can be called by another contract with no data", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called by static call with no data", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + ); + } + if (!receiveFunction && !fallbackFunction) { + specialFunctionTests.push( + { + name: "can not receive zero value transfers with no data", + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas }), + expectedStatus: "reverted", + }, + { + name: "can not receive zero value transfers by high level contract call with no data", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not receive zero value transfers by static call with no data", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + ); + } + + // No receive function AND no payable fallback + if (!receiveFunction && (!fallbackFunction || fallbackFunction.stateMutability !== "payable")) { + specialFunctionTests.push( + { + name: "can not receive plain transfers", + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), + expectedStatus: "reverted", + }, + { + name: "can not receive plain transfers via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedStatus: "reverted", + }, + ); + } + + if (receiveFunction || (fallbackFunction && fallbackFunction.stateMutability === "payable")) { + specialFunctionTests.push( + { + name: "can receive plain transfers", + txParams: (ctx) => ({ to: ctx.address, gas: defaultGas, value: 1n }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can receive plain transfers via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + ); + } + + if (fallbackFunction) { + specialFunctionTests.push( + { + name: "can be called with a non-matching function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with a non-matching function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with a non-matching function selector via static call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with an invalid (short) function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with an invalid (short) function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + { + name: "can be called with an invalid (short) function selector via static call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "success", + }, + ); + } + + if (!fallbackFunction) { + specialFunctionTests.push( + { + name: "can not be called with a non-matching function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not be called with a non-matching function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not be called with a non-matching function selector via static call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not be called with an invalid (short) function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not be called with an invalid (short) function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + { + name: "can not be called with an invalid (short) function selector via static call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionStaticCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + }), + expectedStatus: "reverted", + }, + ); + } + + if (fallbackFunction && fallbackFunction.stateMutability === "payable") { + specialFunctionTests.push( + { + name: "can receive value with a non-matching function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can receive value with a non-matching function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can receive value with an invalid (short) function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + { + name: "can receive value with an invalid (short) function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance + 1n, + expectedStatus: "success", + }, + ); + } + + if (fallbackFunction && fallbackFunction.stateMutability !== "payable") { + specialFunctionTests.push( + { + name: "can not receive value with a non-matching function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: toFunctionSelector("does_not_exist()"), + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can not receive value with a non-matching function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, toFunctionSelector("does_not_exist()")], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can not receive value with an invalid (short) function selector", + txParams: (ctx) => ({ + to: ctx.address, + data: "0x010203", + gas: defaultGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + { + name: "can not receive value with an invalid (short) function selector via message call", + txParams: (ctx) => ({ + to: caller.address, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "functionCall", + args: [ctx.address, "0x010203"], + }), + gas: contractCallerGas, + value: 1n, + }), + expectedBalance: (startingBalance) => startingBalance, + expectedStatus: "reverted", + }, + ); + } + + for (const testCase of specialFunctionTests) { it(testCase.name, async function () { const startingBalance = await publicClient.getBalance({ address: ctx.address }); From cd15cde6dd9bc142e0c1e39fb95dc771a1f8dd1a Mon Sep 17 00:00:00 2001 From: drklee3 Date: Wed, 9 Oct 2024 12:04:45 -0700 Subject: [PATCH 07/11] refactor: Resolve ABI disabled types & errors Resolves use type casting and unsafe access, validation of revert errors for matches and types --- tests/e2e-evm/test/abi_disabled.test.ts | 111 +++++++++++++++--------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/tests/e2e-evm/test/abi_disabled.test.ts b/tests/e2e-evm/test/abi_disabled.test.ts index 0c69988bf..f938a733f 100644 --- a/tests/e2e-evm/test/abi_disabled.test.ts +++ b/tests/e2e-evm/test/abi_disabled.test.ts @@ -1,13 +1,17 @@ import hre from "hardhat"; import type { ArtifactsMap } from "hardhat/types/artifacts"; -import type { - PublicClient, - WalletClient, - ContractName, - GetContractReturnType, -} from "@nomicfoundation/hardhat-viem/types"; +import type { PublicClient, WalletClient, GetContractReturnType } from "@nomicfoundation/hardhat-viem/types"; import { expect } from "chai"; -import { Address, BaseError, Hex, toFunctionSelector, toFunctionSignature, concat, encodeFunctionData } from "viem"; +import { + Address, + BaseError, + Hex, + toFunctionSelector, + toFunctionSignature, + concat, + encodeFunctionData, + isHex, +} from "viem"; import { Abi } from "abitype"; import { getAbiFallbackFunction, getAbiReceiveFunction } from "./helpers/abi"; import { whaleAddress } from "./addresses"; @@ -17,23 +21,23 @@ const messageCallGas = defaultGas + 10000n; interface ContractTestCase { interface: keyof ArtifactsMap; - mock: ContractName; + mock: keyof ArtifactsMap; precompile: Address; } const disabledPrecompiles: Address[] = [ - "0x9000000000000000000000000000000000000007", // noop recieve and payable fallback + "0x9000000000000000000000000000000000000007", // noop receive and payable fallback ]; // ABI_DisabledTests assert ethereum + solidity transaction ABI interactions perform as expected. describe("ABI_DisabledTests", function () { - const testCases = [ + const testCases: ContractTestCase[] = [ { interface: "NoopReceivePayableFallback", // interface to test valid function selectors against mock: "NoopDisabledMock", // mimics how a disabled precompile would behave - precompile: disabledPrecompiles[0], // disabled noop recieve and payable fallback + precompile: disabledPrecompiles[0], // disabled noop receive and payable fallback }, - ] as ContractTestCase[]; + ]; // // Client + Wallet Setup @@ -84,8 +88,10 @@ describe("ABI_DisabledTests", function () { ctx = await getContext(); }); + // testCase is a single test case with the value and data to send. Each of + // these will be tested against each of the callCases. interface testCase { - name: string + name: string; value: bigint; data: Hex; } @@ -94,61 +100,76 @@ describe("ABI_DisabledTests", function () { { name: "value transfer", value: 1n, data: "0x" }, { name: "invalid function selector", value: 0n, data: "0x010203" }, { name: "invalid function selector with value", value: 1n, data: "0x010203" }, - { name: "non-matching function selector", value: 0n, data: toFunctionSelector("does_not_exist()")}, - { name: "non-matching function selector with value", value: 1n, data: toFunctionSelector("does_not_exist()")}, - { name: "non-matching function selector with extra data", value: 0n, data: concat([toFunctionSelector("does_not_exist()"), "0x01"])}, - { name: "non-matching function selector with value and extra data", value: 1n, data: concat([toFunctionSelector("does_not_exist()"), "0x01"])}, + { name: "non-matching function selector", value: 0n, data: toFunctionSelector("does_not_exist()") }, + { name: "non-matching function selector with value", value: 1n, data: toFunctionSelector("does_not_exist()") }, + { + name: "non-matching function selector with extra data", + value: 0n, + data: concat([toFunctionSelector("does_not_exist()"), "0x01"]), + }, + { + name: "non-matching function selector with value and extra data", + value: 1n, + data: concat([toFunctionSelector("does_not_exist()"), "0x01"]), + }, ]; for (const funcDesc of abi) { - if (funcDesc.type !== "function") continue; + if (funcDesc.type !== "function") { + continue; + } + const funcSelector = toFunctionSelector(toFunctionSignature(funcDesc)); - testCases.concat([ + + testCases.push( { name: funcDesc.name, value: 0n, data: funcSelector }, { name: `${funcDesc.name} with value`, value: 1n, data: funcSelector }, { name: `${funcDesc.name} with extra data`, value: 0n, data: concat([funcSelector, "0x01"]) }, { name: `${funcDesc.name} with value and extra data`, value: 1n, data: concat([funcSelector, "0x01"]) }, - ]); + ); } const callCases: { name: string; - mutateData: (tc: Hex) => Promise; - to: () => Promise
; + mutateData: (tc: Hex) => Hex; + to: () => Address; gas: bigint; }[] = [ { name: "external call", - to: async () => ctx.address, - mutateData: async (data) => data, + to: () => ctx.address, + mutateData: (data) => data, gas: defaultGas, }, { name: "message call", - to: async () => caller.address, - mutateData: async (data) => encodeFunctionData({ abi: caller.abi, functionName: "functionCall", args: [ctx.address, data] }), + to: () => caller.address, + mutateData: (data) => + encodeFunctionData({ abi: caller.abi, functionName: "functionCall", args: [ctx.address, data] }), gas: messageCallGas, }, { name: "message delegatecall", - to: async () => caller.address, - mutateData: async (data) => encodeFunctionData({ abi: caller.abi, functionName: "functionDelegateCall", args: [ctx.address, data] }), + to: () => caller.address, + mutateData: (data) => + encodeFunctionData({ abi: caller.abi, functionName: "functionDelegateCall", args: [ctx.address, data] }), gas: messageCallGas, }, { name: "message staticcall", - to: async () => caller.address, - mutateData: async (data) => encodeFunctionData({ abi: caller.abi, functionName: "functionStaticCall", args: [ctx.address, data] }), + to: () => caller.address, + mutateData: (data) => + encodeFunctionData({ abi: caller.abi, functionName: "functionStaticCall", args: [ctx.address, data] }), gas: messageCallGas, }, ]; for (const tc of testCases) { - describe(tc.name, function() { + describe(tc.name, function () { for (const cc of callCases) { - it(`reverts on ${cc.name}`, async function() { - const to = await cc.to(); - const data = await cc.mutateData(tc.data); + it(`reverts on ${cc.name}`, async function () { + const to = cc.to(); + const data = cc.mutateData(tc.data); const txData = { to: to, data: data, gas: cc.gas }; const startingBalance = await publicClient.getBalance({ address: ctx.address }); @@ -165,14 +186,16 @@ describe("ABI_DisabledTests", function () { let revertDetail = ""; try { await call; - } catch(e) { + } catch (e) { + expect(e).to.be.instanceOf(BaseError, "expected error to be a BaseError"); + if (e instanceof BaseError) { revertDetail = e.details; } } - const expectedMatch = /call not allowed to disabled contract/; - expect(expectedMatch.test(revertDetail), `expected ${revertDetail} to match ${expectedMatch}`).to.be.true; + const expectedMatch = "call not allowed to disabled contract"; + expect(revertDetail).to.equal(revertDetail, `expected ${revertDetail} to match ${expectedMatch}`); }); } }); @@ -198,10 +221,16 @@ describe("ABI_DisabledTests", function () { let deployedBytecode: Hex; before("deploy mock", async function () { - mockAddress = (await hre.viem.deployContract(tc.mock)).address; - // TODO: fix typing and do not use explicit any, unsafe assignment, or unsafe access - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - deployedBytecode = ((await hre.artifacts.readArtifact(tc.mock)) as any).deployedBytecode; + const mockContractName: string = tc.mock; + + mockAddress = (await hre.viem.deployContract(mockContractName)).address; + + const mockArtifact = await hre.artifacts.readArtifact(mockContractName); + if (isHex(mockArtifact.deployedBytecode)) { + deployedBytecode = mockArtifact.deployedBytecode; + } else { + expect.fail("deployedBytecode is not hex"); + } }); itHasCorrectState(() => From 0f248d213989a9894ab6389062bdcd54ff42d48c Mon Sep 17 00:00:00 2001 From: drklee3 Date: Wed, 9 Oct 2024 13:46:07 -0700 Subject: [PATCH 08/11] refactor: Only test receive, fallback funcs when available Previously runs all the time, which is currently okay with the current single testing contract that includes both functions. This conditionally adds these test cases if the respective functions exist so we can test additional contract behavior that may not have these functions and may produce a different error. --- tests/e2e-evm/test/abi_disabled.test.ts | 45 +++++++++++++++---------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/e2e-evm/test/abi_disabled.test.ts b/tests/e2e-evm/test/abi_disabled.test.ts index f938a733f..b58335e92 100644 --- a/tests/e2e-evm/test/abi_disabled.test.ts +++ b/tests/e2e-evm/test/abi_disabled.test.ts @@ -95,24 +95,33 @@ describe("ABI_DisabledTests", function () { value: bigint; data: Hex; } - const testCases: testCase[] = [ - { name: "zero value transfer", value: 0n, data: "0x" }, - { name: "value transfer", value: 1n, data: "0x" }, - { name: "invalid function selector", value: 0n, data: "0x010203" }, - { name: "invalid function selector with value", value: 1n, data: "0x010203" }, - { name: "non-matching function selector", value: 0n, data: toFunctionSelector("does_not_exist()") }, - { name: "non-matching function selector with value", value: 1n, data: toFunctionSelector("does_not_exist()") }, - { - name: "non-matching function selector with extra data", - value: 0n, - data: concat([toFunctionSelector("does_not_exist()"), "0x01"]), - }, - { - name: "non-matching function selector with value and extra data", - value: 1n, - data: concat([toFunctionSelector("does_not_exist()"), "0x01"]), - }, - ]; + const testCases: testCase[] = []; + + if (receiveFunction) { + testCases.push( + { name: "zero value transfer", value: 0n, data: "0x" }, + { name: "value transfer", value: 1n, data: "0x" }, + ); + } + + if (fallbackFunction) { + testCases.push( + { name: "invalid function selector", value: 0n, data: "0x010203" }, + { name: "invalid function selector with value", value: 1n, data: "0x010203" }, + { name: "non-matching function selector", value: 0n, data: toFunctionSelector("does_not_exist()") }, + { name: "non-matching function selector with value", value: 1n, data: toFunctionSelector("does_not_exist()") }, + { + name: "non-matching function selector with extra data", + value: 0n, + data: concat([toFunctionSelector("does_not_exist()"), "0x01"]), + }, + { + name: "non-matching function selector with value and extra data", + value: 1n, + data: concat([toFunctionSelector("does_not_exist()"), "0x01"]), + }, + ); + } for (const funcDesc of abi) { if (funcDesc.type !== "function") { From 8a90bbc3af66f45bde04f269116e28feb8deb21b Mon Sep 17 00:00:00 2001 From: drklee3 Date: Thu, 10 Oct 2024 14:48:15 -0700 Subject: [PATCH 09/11] lint: Ignore solhint issues for mock contracts Most of these issues are intentional and are okay to ignore. This also sets the solhint ignoreConstructors option to true for the func-visibility rule, as we are using solidity >=0.7.0 --- tests/e2e-evm/.solhint.json | 5 ++++- tests/e2e-evm/contracts/ABI_BasicTests.sol | 15 +++++++++++---- tests/e2e-evm/contracts/ABI_DisabledTests.sol | 19 ++++++++++--------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/e2e-evm/.solhint.json b/tests/e2e-evm/.solhint.json index ce2220e0b..3d0f38d14 100644 --- a/tests/e2e-evm/.solhint.json +++ b/tests/e2e-evm/.solhint.json @@ -1,3 +1,6 @@ { - "extends": "solhint:recommended" + "extends": "solhint:recommended", + "rules": { + "func-visibility": ["warn", { "ignoreConstructors": true }] + } } diff --git a/tests/e2e-evm/contracts/ABI_BasicTests.sol b/tests/e2e-evm/contracts/ABI_BasicTests.sol index 0548dfda0..f1077498d 100644 --- a/tests/e2e-evm/contracts/ABI_BasicTests.sol +++ b/tests/e2e-evm/contracts/ABI_BasicTests.sol @@ -13,8 +13,10 @@ contract Caller { (bool success, bytes memory result) = to.call{value: msg.value}(data); if (!success) { - if (result.length == 0) revert(); + // solhint-disable-next-line gas-custom-errors + if (result.length == 0) revert("reverted with no reason"); + // solhint-disable-next-line no-inline-assembly assembly { revert(add(32, result), mload(result)) } @@ -24,11 +26,14 @@ contract Caller { // TODO: Callcode function functionDelegateCall(address to, bytes calldata data) external { + // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory result) = to.delegatecall(data); if (!success) { - if (result.length == 0) revert(); + // solhint-disable-next-line gas-custom-errors + if (result.length == 0) revert("reverted with no reason"); + // solhint-disable-next-line no-inline-assembly assembly { revert(add(32, result), mload(result)) } @@ -39,8 +44,10 @@ contract Caller { (bool success, bytes memory result) = to.staticcall(data); if (!success) { - if (result.length == 0) revert(); + // solhint-disable-next-line gas-custom-errors + if (result.length == 0) revert("reverted with no reason"); + // solhint-disable-next-line no-inline-assembly assembly { revert(add(32, result), mload(result)) } @@ -52,7 +59,7 @@ contract Caller { // High level caller // contract NoopCaller { - NoopNoReceiveNoFallback target; + NoopNoReceiveNoFallback private target; constructor(NoopNoReceiveNoFallback _target) { target = _target; diff --git a/tests/e2e-evm/contracts/ABI_DisabledTests.sol b/tests/e2e-evm/contracts/ABI_DisabledTests.sol index 408dd65ed..aaa090deb 100644 --- a/tests/e2e-evm/contracts/ABI_DisabledTests.sol +++ b/tests/e2e-evm/contracts/ABI_DisabledTests.sol @@ -1,37 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import "./ABI_BasicTests.sol"; +import "./ABI_BasicTests.sol" as ABI_BasicTests; // // Disabled contract that is payable and callable with any calldata (receive + fallback) // -contract NoopDisabledMock is NoopReceivePayableFallback{ +contract NoopDisabledMock is ABI_BasicTests.NoopReceivePayableFallback { // solc-ignore-next-line func-mutability function noopNonpayable() external { - mockRevert(); + mockRevert(); } function noopPayable() external payable { - mockRevert(); + mockRevert(); } // solc-ignore-next-line func-mutability function noopView() external view { - mockRevert(); + mockRevert(); } function noopPure() external pure { - mockRevert(); + mockRevert(); } receive() external payable { - mockRevert(); + mockRevert(); } fallback() external payable { - mockRevert(); + mockRevert(); } // // Mimic revert + revert reason // function mockRevert() private pure { - revert("call not allowed to disabled contract"); + // solhint-disable-next-line reason-string, gas-custom-errors + revert("call not allowed to disabled contract"); } } From 1d9834062260574395df387823aa13a653952a4d Mon Sep 17 00:00:00 2001 From: drklee3 Date: Fri, 11 Oct 2024 11:13:02 -0700 Subject: [PATCH 10/11] refactor: Remove unused empty ContractTestCase generic brackets --- tests/e2e-evm/test/abi_basic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts index deaadc059..185232f94 100644 --- a/tests/e2e-evm/test/abi_basic.test.ts +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -20,7 +20,7 @@ import { whaleAddress } from "./addresses"; const defaultGas = 25000n; const contractCallerGas = defaultGas + 10000n; -interface ContractTestCase<> { +interface ContractTestCase { interface: keyof ArtifactsMap; // Ensures contract name ends with "Mock", but does not enforce the prefix // matches the interface. From 70f72d8c6028d63d6366d55f849f3d94d0f24212 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Fri, 11 Oct 2024 11:14:41 -0700 Subject: [PATCH 11/11] chore: Disable chai assertion error truncation Long errors are truncated and difficult to determine the issue otherwise --- tests/e2e-evm/hardhat.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e-evm/hardhat.config.ts b/tests/e2e-evm/hardhat.config.ts index 10b776fb5..f5cc853c2 100644 --- a/tests/e2e-evm/hardhat.config.ts +++ b/tests/e2e-evm/hardhat.config.ts @@ -10,6 +10,7 @@ import "hardhat-ignore-warnings"; // Chai setup // chai.use(chaiAsPromised); +chai.config.truncateThreshold = 0; // // Load HRE extensions