From ee4fc56f013c06a093f3b5a1202652d7e1e81124 Mon Sep 17 00:00:00 2001 From: Nick DeLuca Date: Fri, 20 Sep 2024 13:09:40 -0700 Subject: [PATCH] test: Add auto-generating basic abi tests for solidity interfaces (#2018) * chore: Prefer equal to eq matcher The equal matcher is a bit clearer and more consistent. * chore: Rename viem extension and add deploy This moves the viem extension to a more clear place and adds viem support for deploy contract on kvtool. * chore: Fix the viem extensions path This was changed in a previous commit, but forgot to add to the hardhat config. * chore: Add abitype explicit dependency This is used by viem and required by the added test helpers as well as typing for using ABI artifacts. * test: Document EOA account call behavior These tests assert and document the behavior of empty and eoa accounts with value when called by external transactions with or without data. In addition, they assert the gas usage is as expected and only depedent on the base transaction cost plus calldata cost since no code is executed. * test: Add abi basic tests based on sol interfaces These tests are added to exercise solidity function ABI's for function and special function calling. This is in preperation for expanding these tests to work against stateful precompiles, ensuring that precompile contracts respect ABI function and special function calling conventions. These tests also serve to increase the test coverage of the Kava EVM and document behavior for future code readers. * chore: Remove unused imports These were copied from the empty account tests but usage was removed. * chore: Add solhint to CI and fix CI lint Solhint previously failed due to no contracts, so this adds solhint to CI now that contracts exist. In addition, now that tyepscript uses abitypes from compiled contracts, hardhat compile needs to be run before typescript lint. * chore: format fixes for function parethesis A common typo on my part that prettier keeps catching. * chore: Explain throwOnTransactionFailures setting This adds a comment to explain that this setting is required for hardhat-viem to not throw an error when sending transactions that will revert to match kava chain behavior. * chore: Remove outdated comment This removes a comment reference to the tseslint recommended config that is not needed as we are staying with stricter settings. * lint: Add eqeqeq smart validation and fix == This adds a linter for smart validation of === vs ==. The linter was verified to fail the comparison in this commit and then verified it passes after the change from == to ===. * chore: Add stricter settings for tsconfig This adds noFallthroughCasesInSwitch for switch statements and turns on isolatedModules. * ci: Target the local build image for e2e-evm This sets the KAVA_TAG to correctly target the image built locally in CI when running e2e-evm tests for kvtool testnet up and down. * doc: Add act cmds for running e2e-evm CI jobs This documents the commands for using act to run the github CI jobs related to e2e-evm locally for lint and e2e tests. * lint: Fix readme prettier formatting I guess double spaces between sentances are not liked by prettier. * ci: Add explicit compile step to fix race cond There is a race condition between the typechecking and compiler so we add an explicit compile step here to ensure the race is not hit. * test: Add empty account creation transaction This adds a test that creates an account via a transaction with non-zero value. We test that the account has a balance (is created) and that the gas charged is the 21000 instrinsic. --- .github/workflows/ci-default.yml | 5 + .github/workflows/ci-lint.yml | 6 + tests/e2e-evm/README.md | 11 + tests/e2e-evm/contracts/ABI_BasicTests.sol | 78 +++++ tests/e2e-evm/eslint.config.mjs | 6 +- tests/e2e-evm/hardhat.config.ts | 8 +- tests/e2e-evm/package-lock.json | 290 +++++++++++++++++- tests/e2e-evm/package.json | 3 + tests/e2e-evm/test/abi_basic.test.ts | 162 ++++++++++ tests/e2e-evm/test/connect.test.ts | 10 +- tests/e2e-evm/test/empty_account.test.ts | 94 ++++++ tests/e2e-evm/test/eoa_account.test.ts | 82 +++++ .../test/{extend.ts => extensions/viem.ts} | 19 +- tests/e2e-evm/test/helpers/abi.test.ts | 64 ++++ tests/e2e-evm/test/helpers/abi.ts | 21 ++ tests/e2e-evm/test/helpers/tx.ts | 0 tests/e2e-evm/tsconfig.json | 4 +- 17 files changed, 844 insertions(+), 19 deletions(-) create mode 100644 tests/e2e-evm/contracts/ABI_BasicTests.sol create mode 100644 tests/e2e-evm/test/abi_basic.test.ts create mode 100644 tests/e2e-evm/test/empty_account.test.ts create mode 100644 tests/e2e-evm/test/eoa_account.test.ts rename tests/e2e-evm/test/{extend.ts => extensions/viem.ts} (68%) create mode 100644 tests/e2e-evm/test/helpers/abi.test.ts create mode 100644 tests/e2e-evm/test/helpers/abi.ts create mode 100644 tests/e2e-evm/test/helpers/tx.ts 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 } }