diff --git a/.github/workflows/ci-default.yml b/.github/workflows/ci-default.yml index cb100aedf8..3bfac82330 100644 --- a/.github/workflows/ci-default.yml +++ b/.github/workflows/ci-default.yml @@ -37,6 +37,8 @@ jobs: run: make docker-build test-e2e test-e2e-evm: runs-on: ubuntu-latest + env: + KAVA_TAG: local steps: - name: Checkout current commit uses: actions/checkout@v4 @@ -58,6 +60,9 @@ jobs: - name: Install npm dependencies run: npm install working-directory: tests/e2e-evm + - name: Run test suite against hardhat network + run: npm run compile + working-directory: tests/e2e-evm - name: Run test suite against hardhat network run: npm run test-hardhat working-directory: tests/e2e-evm diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 9678d93032..b28e838388 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -45,6 +45,12 @@ jobs: - name: Install npm dependencies run: npm install working-directory: tests/e2e-evm + - name: Run solhint + run: npm run solhint + working-directory: tests/e2e-evm + - name: Compile contracts and create artifcats + run: npm run compile + working-directory: tests/e2e-evm - name: Run linter run: npm run lint working-directory: tests/e2e-evm diff --git a/tests/e2e-evm/README.md b/tests/e2e-evm/README.md index 17a1fcac68..bd3e160e03 100644 --- a/tests/e2e-evm/README.md +++ b/tests/e2e-evm/README.md @@ -19,3 +19,14 @@ npx hardhat test --network hardhat ``` npx hardhat test --network kvtool ``` + +## Running CI Locally + +With act installed, the following commands will run the lint and e2e CI jobs locally. + +``` +act -W '.github/workflows/ci-lint.yml' -j e2e-evm-lint +act -W '.github/workflows/ci-default.yml' -j test-e2e-evm --bind +``` + +The `--bind` flag is required for volume mounts of docker containers correctly mount. Without this flag, volumes are mounted as an empty directory. diff --git a/tests/e2e-evm/contracts/ABI_BasicTests.sol b/tests/e2e-evm/contracts/ABI_BasicTests.sol new file mode 100644 index 0000000000..b25cff8b13 --- /dev/null +++ b/tests/e2e-evm/contracts/ABI_BasicTests.sol @@ -0,0 +1,78 @@ +// solhint-disable one-contract-per-file +// solhint-disable no-empty-blocks +// solhint-disable payable-fallback + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +// +// Normal noop functions only with nonpayable, payable, view, and pure modifiers +// +interface NoopNoReceiveNoFallback { + function noopNonpayable() external; + function noopPayable() external payable; + function noopView() external view; + function noopPure() external pure; +} +contract NoopNoReceiveNoFallbackMock is NoopNoReceiveNoFallback { + function noopNonpayable() external {} + function noopPayable() external payable {} + function noopView() external view {} + function noopPure() external pure {} +} + +// +// Added receive function (always payable) +// +interface NoopReceiveNoFallback is NoopNoReceiveNoFallback { + receive() external payable; +} +contract NoopReceiveNoFallbackMock is NoopReceiveNoFallback, NoopNoReceiveNoFallbackMock { + receive() external payable {} +} + +// +// Added receive function and payable fallback +// +interface NoopReceivePayableFallback is NoopNoReceiveNoFallback { + receive() external payable; + fallback() external payable; +} +contract NoopReceivePayableFallbackMock is NoopReceivePayableFallback, NoopNoReceiveNoFallbackMock { + receive() external payable {} + fallback() external payable {} +} + +// +// Added receive function and non-payable fallback +// +interface NoopReceiveNonpayableFallback is NoopNoReceiveNoFallback { + receive() external payable; + fallback() external; +} +contract NoopReceiveNonpayableFallbackMock is NoopReceiveNonpayableFallback, NoopNoReceiveNoFallbackMock { + receive() external payable {} + fallback() external {} +} + +// +// Added payable fallback and no receive function +// +// solc-ignore-next-line missing-receive +interface NoopNoReceivePayableFallback is NoopNoReceiveNoFallback { + fallback() external payable; +} +// solc-ignore-next-line missing-receive +contract NoopNoReceivePayableFallbackMock is NoopNoReceivePayableFallback, NoopNoReceiveNoFallbackMock { + fallback() external payable {} +} + +// +// Added non-payable fallback and no receive function +// +interface NoopNoReceiveNonpayableFallback is NoopNoReceiveNoFallback { + fallback() external; +} +contract NoopNoReceiveNonpayableFallbackMock is NoopNoReceiveNonpayableFallback, NoopNoReceiveNoFallbackMock { + fallback() external {} +} diff --git a/tests/e2e-evm/eslint.config.mjs b/tests/e2e-evm/eslint.config.mjs index e4024f318a..dd7e8f7a63 100644 --- a/tests/e2e-evm/eslint.config.mjs +++ b/tests/e2e-evm/eslint.config.mjs @@ -3,7 +3,11 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, - //...tseslint.configs.recommendedTypeChecked, + { + rules: { + eqeqeq: ["error", "smart"], + }, + }, ...tseslint.configs.strictTypeChecked, ...tseslint.configs.stylisticTypeChecked, { diff --git a/tests/e2e-evm/hardhat.config.ts b/tests/e2e-evm/hardhat.config.ts index 014b1ed3db..10b776fb58 100644 --- a/tests/e2e-evm/hardhat.config.ts +++ b/tests/e2e-evm/hardhat.config.ts @@ -1,9 +1,10 @@ import { HardhatUserConfig, extendEnvironment } from "hardhat/config"; import "@nomicfoundation/hardhat-viem"; import { parseEther } from "viem"; -import { extendViem } from "./test/extend"; +import { extendViem } from "./test/extensions/viem"; import chai from "chai"; import chaiAsPromised from "chai-as-promised"; +import "hardhat-ignore-warnings"; // // Chai setup @@ -56,6 +57,11 @@ const config: HardhatUserConfig = { chainId: 31337, // The default hardhat network chain id hardfork: "berlin", // The current hardfork of kava mainnet accounts: accounts, + // + // This is required for hardhat-viem to have the same behavior + // for reverted transactions as on Kava. + // + throwOnTransactionFailures: false, }, kvtool: { chainId: 8888, // The evm chain id of the kvtool network diff --git a/tests/e2e-evm/package-lock.json b/tests/e2e-evm/package-lock.json index 7e6f46b1d8..4d2229a166 100644 --- a/tests/e2e-evm/package-lock.json +++ b/tests/e2e-evm/package-lock.json @@ -16,10 +16,12 @@ "@types/eslint__js": "^8.42.3", "@types/mocha": "^10.0.7", "@types/node": "^20.0.0", + "abitype": "^1.0.6", "chai": "^4.3.0", "chai-as-promised": "^7.1.2", "eslint": "^9.9.1", "hardhat": "^2.22.9", + "hardhat-ignore-warnings": "^0.2.11", "prettier": "3.3.3", "prettier-plugin-solidity": "^1.4.1", "solhint": "^5.0.3", @@ -948,6 +950,31 @@ "viem": "^2.7.6" } }, + "node_modules/@nomicfoundation/hardhat-viem/node_modules/abitype": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.10.tgz", + "integrity": "sha512-FIS7U4n7qwAT58KibwYig5iFG4K61rbhAqaQh/UWj8v1Y8mjX3F8TC9gd8cz9yT1TYel9f8nS5NO5kZp2RW0jQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", @@ -1630,16 +1657,14 @@ } }, "node_modules/abitype": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.10.tgz", - "integrity": "sha512-FIS7U4n7qwAT58KibwYig5iFG4K61rbhAqaQh/UWj8v1Y8mjX3F8TC9gd8cz9yT1TYel9f8nS5NO5kZp2RW0jQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" @@ -3404,6 +3429,41 @@ } } }, + "node_modules/hardhat-ignore-warnings": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.11.tgz", + "integrity": "sha512-+nHnRbP6COFZaXE7HAY7TZNE3au5vHe5dkcnyq0XaP07ikT2fJ3NhFY0vn7Deh4Qbz0Z/9Xpnj2ki6Ktgk61pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "node-interval-tree": "^2.0.1", + "solidity-comments": "^0.0.2" + } + }, + "node_modules/hardhat-ignore-warnings/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/hardhat-ignore-warnings/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4338,6 +4398,19 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-interval-tree": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-interval-tree/-/node-interval-tree-2.1.2.tgz", + "integrity": "sha512-bJ9zMDuNGzVQg1xv0bCPzyEDxHgbrx7/xGj6CDokvizZZmastPsOh0JJLuY8wA5q2SfX1TLNMk7XNV8WxbGxzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shallowequal": "^1.1.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5045,6 +5118,13 @@ "sha.js": "bin.js" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5345,6 +5425,198 @@ "node": ">=8" } }, + "node_modules/solidity-comments": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments/-/solidity-comments-0.0.2.tgz", + "integrity": "sha512-G+aK6qtyUfkn1guS8uzqUeua1dURwPlcOjoTYW/TwmXAcE7z/1+oGCfZUdMSe4ZMKklNbVZNiG5ibnF8gkkFfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "optionalDependencies": { + "solidity-comments-darwin-arm64": "0.0.2", + "solidity-comments-darwin-x64": "0.0.2", + "solidity-comments-freebsd-x64": "0.0.2", + "solidity-comments-linux-arm64-gnu": "0.0.2", + "solidity-comments-linux-arm64-musl": "0.0.2", + "solidity-comments-linux-x64-gnu": "0.0.2", + "solidity-comments-linux-x64-musl": "0.0.2", + "solidity-comments-win32-arm64-msvc": "0.0.2", + "solidity-comments-win32-ia32-msvc": "0.0.2", + "solidity-comments-win32-x64-msvc": "0.0.2" + } + }, + "node_modules/solidity-comments-darwin-arm64": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-darwin-arm64/-/solidity-comments-darwin-arm64-0.0.2.tgz", + "integrity": "sha512-HidWkVLSh7v+Vu0CA7oI21GWP/ZY7ro8g8OmIxE8oTqyMwgMbE8F1yc58Sj682Hj199HCZsjmtn1BE4PCbLiGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-darwin-x64": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-darwin-x64/-/solidity-comments-darwin-x64-0.0.2.tgz", + "integrity": "sha512-Zjs0Ruz6faBTPT6fBecUt6qh4CdloT8Bwoc0+qxRoTn9UhYscmbPQkUgQEbS0FQPysYqVzzxJB4h1Ofbf4wwtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-freebsd-x64": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-freebsd-x64/-/solidity-comments-freebsd-x64-0.0.2.tgz", + "integrity": "sha512-8Qe4mpjuAxFSwZJVk7B8gAoLCdbtS412bQzBwk63L8dmlHogvE39iT70aAk3RHUddAppT5RMBunlPUCFYJ3ZTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-linux-arm64-gnu": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-linux-arm64-gnu/-/solidity-comments-linux-arm64-gnu-0.0.2.tgz", + "integrity": "sha512-spkb0MZZnmrP+Wtq4UxP+nyPAVRe82idOjqndolcNR0S9Xvu4ebwq+LvF4HiUgjTDmeiqYiFZQ8T9KGdLSIoIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-linux-arm64-musl": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-linux-arm64-musl/-/solidity-comments-linux-arm64-musl-0.0.2.tgz", + "integrity": "sha512-guCDbHArcjE+JDXYkxx5RZzY1YF6OnAKCo+sTC5fstyW/KGKaQJNPyBNWuwYsQiaEHpvhW1ha537IvlGek8GqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-linux-x64-gnu": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-linux-x64-gnu/-/solidity-comments-linux-x64-gnu-0.0.2.tgz", + "integrity": "sha512-zIqLehBK/g7tvrFmQljrfZXfkEeLt2v6wbe+uFu6kH/qAHZa7ybt8Vc0wYcmjo2U0PeBm15d79ee3AkwbIjFdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-linux-x64-musl": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-linux-x64-musl/-/solidity-comments-linux-x64-musl-0.0.2.tgz", + "integrity": "sha512-R9FeDloVlFGTaVkOlELDVC7+1Tjx5WBPI5L8r0AGOPHK3+jOcRh6sKYpI+VskSPDc3vOO46INkpDgUXrKydlIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-win32-arm64-msvc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-win32-arm64-msvc/-/solidity-comments-win32-arm64-msvc-0.0.2.tgz", + "integrity": "sha512-QnWJoCQcJj+rnutULOihN9bixOtYWDdF5Rfz9fpHejL1BtNjdLW1om55XNVHGAHPqBxV4aeQQ6OirKnp9zKsug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-win32-ia32-msvc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-win32-ia32-msvc/-/solidity-comments-win32-ia32-msvc-0.0.2.tgz", + "integrity": "sha512-vUg4nADtm/NcOtlIymG23NWJUSuMsvX15nU7ynhGBsdKtt8xhdP3C/zA6vjDk8Jg+FXGQL6IHVQ++g/7rSQi0w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/solidity-comments-win32-x64-msvc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/solidity-comments-win32-x64-msvc/-/solidity-comments-win32-x64-msvc-0.0.2.tgz", + "integrity": "sha512-36j+KUF4V/y0t3qatHm/LF5sCUCBx2UndxE1kq5bOzh/s+nQgatuyB+Pd5BfuPQHdWu2KaExYe20FlAa6NL7+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/tests/e2e-evm/package.json b/tests/e2e-evm/package.json index 585c32c757..710879d9aa 100644 --- a/tests/e2e-evm/package.json +++ b/tests/e2e-evm/package.json @@ -7,6 +7,7 @@ "description": "An e2e test suite for the Kava protocol and blockchain.", "private": true, "scripts": { + "compile": "hardhat compile", "lint": "eslint .", "lint-fix": "eslint --fix .", "prettier": "prettier '**/*.{json,sol,md,ts,js}' --check", @@ -26,10 +27,12 @@ "@types/eslint__js": "^8.42.3", "@types/mocha": "^10.0.7", "@types/node": "^20.0.0", + "abitype": "^1.0.6", "chai": "^4.3.0", "chai-as-promised": "^7.1.2", "eslint": "^9.9.1", "hardhat": "^2.22.9", + "hardhat-ignore-warnings": "^0.2.11", "prettier": "3.3.3", "prettier-plugin-solidity": "^1.4.1", "solhint": "^5.0.3", diff --git a/tests/e2e-evm/test/abi_basic.test.ts b/tests/e2e-evm/test/abi_basic.test.ts new file mode 100644 index 0000000000..b1d229ef06 --- /dev/null +++ b/tests/e2e-evm/test/abi_basic.test.ts @@ -0,0 +1,162 @@ +import hre from "hardhat"; +import type { ArtifactsMap } from "hardhat/types/artifacts"; +import type { PublicClient, WalletClient, ContractName } from "@nomicfoundation/hardhat-viem/types"; +import { expect } from "chai"; +import { Address, toFunctionSelector, toFunctionSignature, concat } from "viem"; +import { getAbiFallbackFunction, getAbiReceiveFunction } from "./helpers/abi"; +import { whaleAddress } from "./addresses"; + +interface ContractTestCase { + interface: keyof ArtifactsMap; + mock: ContractName; +} + +// ABI_BasicTests assert ethereum + solidity transaction ABI interactions perform as expected. +describe("ABI_BasicTests", function () { + const testCases = [ + // Test function modifiers without receive & fallback + { interface: "NoopNoReceiveNoFallback", mock: "NoopNoReceiveNoFallbackMock" }, + + // Test receive + fallback scenarios + { interface: "NoopReceiveNoFallback", mock: "NoopReceiveNoFallbackMock" }, + { interface: "NoopReceivePayableFallback", mock: "NoopReceivePayableFallbackMock" }, + { interface: "NoopReceiveNonpayableFallback", mock: "NoopReceiveNonpayableFallbackMock" }, + + // Test no receive + fallback scenarios + { interface: "NoopNoReceivePayableFallback", mock: "NoopNoReceivePayableFallbackMock" }, + { interface: "NoopNoReceiveNonpayableFallback", mock: "NoopNoReceiveNonpayableFallbackMock" }, + ] as ContractTestCase[]; + + // + // Client + Wallet Setup + // + let publicClient: PublicClient; + let walletClient: WalletClient; + before("setup clients", async function () { + publicClient = await hre.viem.getPublicClient(); + walletClient = await hre.viem.getWalletClient(whaleAddress); + }); + + // + // Test each function defined in the interface ABI + // + // Only payable functions may be sent value + // Any function (payable, non-payable, view, pure) can be called via a transaction + // All functions can be provided calldata regardless of their arguments + for (const tc of testCases) { + describe(tc.interface, function () { + const abi = hre.artifacts.readArtifactSync(tc.interface).abi; + const receiveFunction = getAbiReceiveFunction(abi); + const fallbackFunction = getAbiFallbackFunction(abi); + + let mockAddress: Address; + before("deploy mock", async function () { + mockAddress = (await hre.viem.deployContract(tc.mock)).address; + }); + + describe("State", function () { + it("has code set", async function () { + const code = await publicClient.getCode({ address: mockAddress }); + expect(code).to.not.equal(0); + }); + + it("has nonce default of 1", async function () { + const nonce = await publicClient.getTransactionCount({ address: mockAddress }); + expect(nonce).to.equal(1); + }); + + it("has a starting balance of 0", async function () { + const balance = await publicClient.getBalance({ address: mockAddress }); + expect(balance).to.equal(0n); + }); + }); + + for (const funcDesc of abi) { + if (funcDesc.type !== "function") { + continue; + } + + const funcSelector = toFunctionSelector(toFunctionSignature(funcDesc)); + + describe(`ABI function ${funcDesc.name} ${funcDesc.stateMutability}`, function () { + const isPayable = funcDesc.stateMutability === "payable"; + + it("can be called", async function () { + const txData = { to: mockAddress, data: funcSelector, gas: 25000n }; + + 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"); + }); + + it(`can ${isPayable ? "" : "not "}be called with value`, async function () { + const txData = { to: mockAddress, data: funcSelector, gas: 25000n, value: 1n }; + + const txHash = await walletClient.sendTransaction(txData); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + expect(txReceipt.status).to.equal(isPayable ? "success" : "reverted"); + }); + + it("can be called with extra data", async function () { + const data = concat([funcSelector, "0x01"]); + const txData = { to: mockAddress, data: data, gas: 25000n }; + + const txHash = await walletClient.sendTransaction(txData); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + expect(txReceipt.status).to.equal("success"); + }); + }); + } + + // + // Test ABI special functions -- receive & fallback + // + // Receive functions are always payable and can not receive data + // 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 || fallbackFunction.stateMutability !== "payable")) { + it("can not receive plain transfers", async function () { + const txData = { to: mockAddress, gas: 25000n, value: 1n }; + + const txHash = await walletClient.sendTransaction(txData); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + expect(txReceipt.status).to.equal("reverted"); + }); + } + + if (receiveFunction || (fallbackFunction && fallbackFunction.stateMutability === "payable")) { + it("can receive plain transfers", async function () { + const txData = { to: mockAddress, gas: 25000n, value: 1n }; + + const txHash = await walletClient.sendTransaction(txData); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + expect(txReceipt.status).to.equal("success"); + }); + } + + it(`can ${fallbackFunction ? "" : "not "}be called with an invalid function selector`, async function () { + const data = toFunctionSelector("does_not_exist()"); + const txData = { to: mockAddress, data: data, gas: 25000n }; + + const txHash = await walletClient.sendTransaction(txData); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + expect(txReceipt.status).to.equal(fallbackFunction ? "success" : "reverted"); + }); + + if (fallbackFunction) { + it(`can ${fallbackFunction.stateMutability === "payable" ? "" : "not "}receive transfer with data or invalid function selector`, async function () { + const data = toFunctionSelector("does_not_exist()"); + const txData = { to: mockAddress, data: data, gas: 25000n, value: 1n }; + + const txHash = await walletClient.sendTransaction(txData); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + expect(txReceipt.status).to.equal(fallbackFunction.stateMutability === "payable" ? "success" : "reverted"); + }); + } + }); + }); + } +}); diff --git a/tests/e2e-evm/test/connect.test.ts b/tests/e2e-evm/test/connect.test.ts index f6a0bdea0f..0a33977a2f 100644 --- a/tests/e2e-evm/test/connect.test.ts +++ b/tests/e2e-evm/test/connect.test.ts @@ -15,8 +15,8 @@ describe("Viem Setup", function () { } })(); - expect(hre.network.config.chainId).to.eq(expectedChainId); - expect(await publicClient.getChainId()).to.eq(expectedChainId); + expect(hre.network.config.chainId).to.equal(expectedChainId); + expect(await publicClient.getChainId()).to.equal(expectedChainId); }); it("is configured with whale and user accounts", async function () { @@ -42,9 +42,9 @@ describe("Viem Setup", function () { const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); const tx = await publicClient.getTransaction({ hash: txHash }); - expect(txReceipt.status).to.eq("success"); - expect(txReceipt.gasUsed).to.eq(21000n); - expect(tx.value).to.eq(tc.value); + expect(txReceipt.status).to.equal("success"); + expect(txReceipt.gasUsed).to.equal(21000n); + expect(tx.value).to.equal(tc.value); }); }); }); diff --git a/tests/e2e-evm/test/empty_account.test.ts b/tests/e2e-evm/test/empty_account.test.ts new file mode 100644 index 0000000000..afeb0914fc --- /dev/null +++ b/tests/e2e-evm/test/empty_account.test.ts @@ -0,0 +1,94 @@ +import hre from "hardhat"; +import { expect } from "chai"; +import { Address, toHex } from "viem"; +import { randomBytes } from "crypto"; +import { whaleAddress } from "./addresses"; + +// Empty Account describes how transactions behave against an account with +// with no balance, no code, and no nonce. +// +// Accounts without code can be called with any data and value. +describe("Empty Account", function () { + const emptyAddress: Address = toHex(randomBytes(20)); + + // The definition of an empty account + it("has no balance, no code, and zero nonce", async function () { + const publicClient = await hre.viem.getPublicClient(); + + const balance = await publicClient.getBalance({ address: emptyAddress }); + const code = await publicClient.getCode({ address: emptyAddress }); + const nonce = await publicClient.getTransactionCount({ address: emptyAddress }); + + expect(balance).to.equal(0n); + expect(code).to.be.undefined; + expect(nonce).to.equal(0); + }); + + // An empty account can receive a 0 value transaction + it("can be called with no data or value", async function () { + const publicClient = await hre.viem.getPublicClient(); + const walletClient = await hre.viem.getWalletClient(whaleAddress); + + const txHash = await walletClient.sendTransaction({ + to: emptyAddress, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + const tx = await publicClient.getTransaction({ hash: txHash }); + + expect(txReceipt.status).to.equal("success"); + expect(txReceipt.gasUsed).to.equal(21000n); + expect(tx.value).to.equal(0n); + expect(tx.to).to.equal(emptyAddress); + expect(tx.input).to.equal("0x"); + }); + + // An empty account can receive a call with any data payload + it("can be called with data", async function () { + const publicClient = await hre.viem.getPublicClient(); + const walletClient = await hre.viem.getWalletClient(whaleAddress); + + // 16 bytes with 1 of them zero + // 16 * 15 + 4 * 1 = 244 gas + const calldata = "0x1eb478108900a0b492ef5dd03921d02d"; + + const txHash = await walletClient.sendTransaction({ + to: emptyAddress, + data: calldata, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + const tx = await publicClient.getTransaction({ hash: txHash }); + + expect(txReceipt.status).to.equal("success"); + // exact gas -- no memory expansion or op code charges + expect(txReceipt.gasUsed).to.equal(21000n + 244n); + expect(tx.value).to.equal(0n); + expect(tx.to).to.equal(emptyAddress); + expect(tx.input).to.equal(calldata); + }); + + // Transaction may create an account at no extra cost + it("can be called with data", async function () { + const publicClient = await hre.viem.getPublicClient(); + const walletClient = await hre.viem.getWalletClient(whaleAddress); + + // Use a random account to not violate the assumption that parent emptyAccount is empty + const emptyAddress = toHex(randomBytes(20)); + + const txHash = await walletClient.sendTransaction({ + to: emptyAddress, + value: 1n, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + const tx = await publicClient.getTransaction({ hash: txHash }); + + expect(txReceipt.status).to.equal("success"); + // A transaction may create an account with no extra charge beyond 21000 instrinic + expect(txReceipt.gasUsed).to.equal(21000n); + expect(tx.value).to.equal(1n); + expect(tx.to).to.equal(emptyAddress); + + // Verify account was committed & value transferred + const balance = await publicClient.getBalance({ address: emptyAddress }); + expect(balance).to.equal(1n); + }); +}); diff --git a/tests/e2e-evm/test/eoa_account.test.ts b/tests/e2e-evm/test/eoa_account.test.ts new file mode 100644 index 0000000000..baadde9056 --- /dev/null +++ b/tests/e2e-evm/test/eoa_account.test.ts @@ -0,0 +1,82 @@ +import hre from "hardhat"; +import { expect } from "chai"; +import { whaleAddress, userAddress } from "./addresses"; + +// EOA Account describes how transactions behave against an account with +// with no code while holding a balance. +// +// Accounts without code can be called with any data and value. +describe("EOA Account", function () { + it("has a balance and no code", async function () { + const publicClient = await hre.viem.getPublicClient(); + + const balance = await publicClient.getBalance({ address: userAddress }); + const code = await publicClient.getCode({ address: userAddress }); + + expect(balance).to.not.equal(0n); + expect(code).to.be.undefined; + }); + + it("can be called with no data or value", async function () { + const publicClient = await hre.viem.getPublicClient(); + const walletClient = await hre.viem.getWalletClient(whaleAddress); + + const txHash = await walletClient.sendTransaction({ + to: userAddress, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + const tx = await publicClient.getTransaction({ hash: txHash }); + + expect(txReceipt.status).to.equal("success"); + expect(txReceipt.gasUsed).to.equal(21000n); + expect(tx.value).to.equal(0n); + expect(tx.to).to.equal(userAddress); + expect(tx.input).to.equal("0x"); + }); + + it("can be called with data", async function () { + const publicClient = await hre.viem.getPublicClient(); + const walletClient = await hre.viem.getWalletClient(whaleAddress); + + // 16 bytes with 1 of them zero + // 16 * 15 + 4 * 1 = 244 gas + const calldata = "0x1eb478108900a0b492ef5dd03921d02d"; + + const txHash = await walletClient.sendTransaction({ + to: userAddress, + data: calldata, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + const tx = await publicClient.getTransaction({ hash: txHash }); + + expect(txReceipt.status).to.equal("success"); + // exact gas -- no memory expansion or op code charges + expect(txReceipt.gasUsed).to.equal(21000n + 244n); + expect(tx.value).to.equal(0n); + expect(tx.to).to.equal(userAddress); + expect(tx.input).to.equal(calldata); + }); + + it("can be called with data and value", async function () { + const publicClient = await hre.viem.getPublicClient(); + const walletClient = await hre.viem.getWalletClient(whaleAddress); + + // 4 non-zero bytes + // 16 * 4 = 64 gas + const calldata = "0x1eb47810"; + + const txHash = await walletClient.sendTransaction({ + to: userAddress, + data: calldata, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + const tx = await publicClient.getTransaction({ hash: txHash }); + + expect(txReceipt.status).to.equal("success"); + // exact gas -- no memory expansion or op code charges + expect(txReceipt.gasUsed).to.equal(21000n + 64n); + expect(tx.value).to.equal(0n); + expect(tx.to).to.equal(userAddress); + expect(tx.input).to.equal(calldata); + }); +}); diff --git a/tests/e2e-evm/test/extend.ts b/tests/e2e-evm/test/extensions/viem.ts similarity index 68% rename from tests/e2e-evm/test/extend.ts rename to tests/e2e-evm/test/extensions/viem.ts index 28f3a7f54e..cf84f2ad6b 100644 --- a/tests/e2e-evm/test/extend.ts +++ b/tests/e2e-evm/test/extensions/viem.ts @@ -1,4 +1,5 @@ import { Address, defineChain, Chain, PublicClientConfig, WalletClientConfig } from "viem"; +import { DeployContractConfig, ContractName } from "@nomicfoundation/hardhat-viem/types"; import { HardhatRuntimeEnvironment } from "hardhat/types/runtime"; // defaultPublicClientConfig sets default values for viem public client configuration @@ -28,7 +29,7 @@ const kavalocalnet: Chain = defineChain({ // getChainConfig returns the kvtoollocalnet Chain if the hardhat environment kvtool network is set, else undefined function getChainConfig(hre: HardhatRuntimeEnvironment): { chain?: Chain } { - if (hre.network.name == "kvtool") { + if (hre.network.name === "kvtool") { return { chain: kavalocalnet }; } @@ -38,7 +39,7 @@ function getChainConfig(hre: HardhatRuntimeEnvironment): { chain?: Chain } { // extendViem wraps the viem hardhat runtime environment in order to support kvtool chain configuration export function extendViem(hre: HardhatRuntimeEnvironment) { /* eslint-disable @typescript-eslint/unbound-method */ - const { getPublicClient, getWalletClients, getWalletClient } = hre.viem; + const { getPublicClient, getWalletClients, getWalletClient, deployContract } = hre.viem; /* eslint-enable @typescript-eslint/unbound-method */ hre.viem.getPublicClient = (publicClientConfig?: Partial) => @@ -49,4 +50,18 @@ export function extendViem(hre: HardhatRuntimeEnvironment) { hre.viem.getWalletClient = (address: Address, walletClientConfig?: Partial) => getWalletClient(address, { ...walletClientConfig, ...getChainConfig(hre) }); + + hre.viem.deployContract = (async ( + contractName: ContractName, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructorArgs: any[] = [], + config: DeployContractConfig = {}, + ) => { + const publicClient = config.client?.public ?? (await hre.viem.getPublicClient()); + const walletClient = config.client?.wallet ?? (await hre.viem.getWalletClients())[0]; + + config.client = { public: publicClient, wallet: walletClient }; + + return deployContract(contractName, constructorArgs, config); + }) as HardhatRuntimeEnvironment["viem"]["deployContract"]; } diff --git a/tests/e2e-evm/test/helpers/abi.test.ts b/tests/e2e-evm/test/helpers/abi.test.ts new file mode 100644 index 0000000000..15ad73bbcb --- /dev/null +++ b/tests/e2e-evm/test/helpers/abi.test.ts @@ -0,0 +1,64 @@ +import { expect } from "chai"; +import { Abi, AbiFallback, AbiFunction, AbiReceive } from "abitype"; +import { getAbiFallbackFunction, getAbiReceiveFunction } from "./abi"; + +const fallback: AbiFallback = { type: "fallback", stateMutability: "payable" }; +const receive: AbiReceive = { type: "receive", stateMutability: "payable" }; +const function1: AbiFunction = { + type: "function", + name: "function1", + inputs: [], + outputs: [], + stateMutability: "nonpayable", +}; +const function2: AbiFunction = { + type: "function", + name: "function2", + inputs: [], + outputs: [], + stateMutability: "payable", +}; + +describe("ABI Helpers", function () { + describe("getAbiFallbackFunction", function () { + const testCases: { + name: string; + abi: Abi; + expectedResult: AbiFallback | undefined; + }[] = [ + { name: "returns undefined with an empty abi", abi: [], expectedResult: undefined }, + { name: "returns undefined with no fallback", abi: [receive, function1, function2], expectedResult: undefined }, + { + name: "returns the fallback function when in abi", + abi: [receive, function1, fallback, function2], + expectedResult: fallback, + }, + { name: "returns the fallback when it is the only function", abi: [fallback], expectedResult: fallback }, + ]; + + for (const tc of testCases) { + it(tc.name, function () { + expect(getAbiFallbackFunction(tc.abi)).to.equal(tc.expectedResult); + }); + } + }); + + describe("getAbiFallbackFunction", function () { + const testCases: { + name: string; + abi: Abi; + expectedResult: AbiReceive | undefined; + }[] = [ + { name: "returns undefined with an empty abi", abi: [], expectedResult: undefined }, + { name: "returns undefined with no fallback", abi: [fallback, function1, function2], expectedResult: undefined }, + { name: "returns receive when in abi", abi: [function1, receive, fallback, function2], expectedResult: receive }, + { name: "returns receive when it is the only function", abi: [receive], expectedResult: receive }, + ]; + + for (const tc of testCases) { + it(tc.name, function () { + expect(getAbiReceiveFunction(tc.abi)).to.equal(tc.expectedResult); + }); + } + }); +}); diff --git a/tests/e2e-evm/test/helpers/abi.ts b/tests/e2e-evm/test/helpers/abi.ts new file mode 100644 index 0000000000..ac0e51a011 --- /dev/null +++ b/tests/e2e-evm/test/helpers/abi.ts @@ -0,0 +1,21 @@ +import { Abi, AbiFallback, AbiReceive } from "abitype"; + +export function getAbiFallbackFunction(abi: Abi): AbiFallback | undefined { + for (const f of abi) { + if (f.type === "fallback") { + return f; + } + } + + return undefined; +} + +export function getAbiReceiveFunction(abi: Abi): AbiReceive | undefined { + for (const f of abi) { + if (f.type === "receive") { + return f; + } + } + + return undefined; +} diff --git a/tests/e2e-evm/test/helpers/tx.ts b/tests/e2e-evm/test/helpers/tx.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e-evm/tsconfig.json b/tests/e2e-evm/tsconfig.json index 0fc1ccaea6..7046c2d722 100644 --- a/tests/e2e-evm/tsconfig.json +++ b/tests/e2e-evm/tsconfig.json @@ -7,6 +7,8 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true } }