From f70ce3f4c4bf46871096d2e5c8a8f246afe90f13 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Tue, 21 Oct 2025 13:33:16 -0400 Subject: [PATCH 1/5] initial commit --- .gitignore | 2 + hardhat.config.ts | 61 + package-lock.json | 4601 +++++++++++++++++ package.json | 48 + test/e2e/README.md | 567 ++ .../interaction/AdvancedInteraction.ts | 192 + .../instances/interaction/ERC20Interaction.ts | 32 + .../interaction/ExtendedInteraction.ts | 260 + .../interaction/SimpleInteraction.ts | 35 + .../interaction/VariationInteraction.ts | 120 + test/e2e/instances/setup/AdvancedSetup.ts | 66 + test/e2e/instances/setup/ERC20Setup.ts | 56 + test/e2e/instances/setup/ExtendedSetup.ts | 122 + test/e2e/instances/setup/SimpleSetup.ts | 51 + test/e2e/instances/setup/VariationSetup.ts | 56 + test/e2e/run-e2e-tests.sh | 55 + test/e2e/schemas/TestInteractionSchema.ts | 176 + test/e2e/schemas/TestSetupSchema.ts | 87 + test/e2e/src/AssertionEngine.ts | 409 ++ test/e2e/src/AuctionDeployer.ts | 707 +++ test/e2e/src/BidSimulator.ts | 481 ++ test/e2e/src/E2ECliRunner.ts | 218 + test/e2e/src/MultiTestRunner.ts | 99 + test/e2e/src/SchemaValidator.ts | 128 + test/e2e/src/SingleTestRunner.ts | 1082 ++++ test/e2e/src/constants.ts | 146 + test/e2e/src/types.ts | 129 + test/e2e/src/utils.ts | 82 + test/e2e/tests/e2e.test.ts | 104 + 29 files changed, 10172 insertions(+) create mode 100644 hardhat.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test/e2e/README.md create mode 100644 test/e2e/instances/interaction/AdvancedInteraction.ts create mode 100644 test/e2e/instances/interaction/ERC20Interaction.ts create mode 100644 test/e2e/instances/interaction/ExtendedInteraction.ts create mode 100644 test/e2e/instances/interaction/SimpleInteraction.ts create mode 100644 test/e2e/instances/interaction/VariationInteraction.ts create mode 100644 test/e2e/instances/setup/AdvancedSetup.ts create mode 100644 test/e2e/instances/setup/ERC20Setup.ts create mode 100644 test/e2e/instances/setup/ExtendedSetup.ts create mode 100644 test/e2e/instances/setup/SimpleSetup.ts create mode 100644 test/e2e/instances/setup/VariationSetup.ts create mode 100755 test/e2e/run-e2e-tests.sh create mode 100644 test/e2e/schemas/TestInteractionSchema.ts create mode 100644 test/e2e/schemas/TestSetupSchema.ts create mode 100644 test/e2e/src/AssertionEngine.ts create mode 100644 test/e2e/src/AuctionDeployer.ts create mode 100644 test/e2e/src/BidSimulator.ts create mode 100644 test/e2e/src/E2ECliRunner.ts create mode 100644 test/e2e/src/MultiTestRunner.ts create mode 100644 test/e2e/src/SchemaValidator.ts create mode 100644 test/e2e/src/SingleTestRunner.ts create mode 100644 test/e2e/src/constants.ts create mode 100644 test/e2e/src/types.ts create mode 100644 test/e2e/src/utils.ts create mode 100644 test/e2e/tests/e2e.test.ts diff --git a/.gitignore b/.gitignore index f24ebc13..0c5add47 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ lcov.info .vscode .password +node_modules/ + broadcast/*/31337 deployments/**/31337.* visualizations/auction_simple.py diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 00000000..7cbb5173 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,61 @@ +// hardhat.config.ts +import "@nomicfoundation/hardhat-foundry"; +import "@nomicfoundation/hardhat-ethers"; +import "@typechain/hardhat"; + +import type { HardhatUserConfig } from "hardhat/config"; +import { subtask } from "hardhat/config"; +import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from "hardhat/builtin-tasks/task-names"; +import path from "node:path"; +import * as glob from "glob"; + +subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS, async (_, { config }) => { + const root = config.paths.root; + const contractSources = glob.sync(path.join(root, "src/**/*.sol")); + const testUtilsSources = glob.sync(path.join(root, "test/utils/**/*.sol")); + return [...contractSources, ...testUtilsSources].map((p) => path.normalize(p)); +}); + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.26", + settings: { + optimizer: { enabled: true, runs: 200 }, + viaIR: true, // Required for complex contracts + }, + }, + networks: { + hardhat: { + blockGasLimit: 40_000_000, + accounts: { + count: 20, + initialIndex: 0, + mnemonic: "test test test test test test test test test test test junk", + path: "m/44'/60'/0'/0", + accountsBalance: "10000000000000000000000", // 10k ETH + }, + }, + }, + paths: { + sources: "./src", + tests: "./test/e2e/tests", + cache: "./test/e2e/cache", + artifacts: "./test/e2e/artifacts", + }, + mocha: { + timeout: 300000, // 5 minutes for complex e2e tests + }, + typechain: { + outDir: "typechain-types", + target: "ethers-v6", + alwaysGenerateOverloads: false, + externalArtifacts: [ + "out/Auction.sol/Auction.json", + "out/AuctionFactory.sol/AuctionFactory.json", + "out/WorkingCustomMockToken.sol/WorkingCustomMockToken.json", + ], // Specific Foundry artifacts + dontOverrideCompile: false, // Let it compile if needed + }, +}; + +export default config; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..e0608a64 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4601 @@ +{ + "name": "twap-auction-e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "twap-auction-e2e", + "version": "1.0.0", + "dependencies": { + "@uniswap/permit2-sdk": "^1.4.0", + "@uniswap/sdk-core": "^7.7.2", + "ethers": "^6.8.0", + "solady": "^0.1.26" + }, + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "@nomicfoundation/hardhat-foundry": "^1.2.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.0", + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.0", + "@types/node": "^20.0.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "chai": "^4.3.7", + "hardhat": "^2.19.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.0", + "typechain": "^8.3.2", + "typescript": "^5.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "5.0.2", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util": { + "version": "9.1.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^5.0.2", + "ethereum-cryptography": "^2.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/curves": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "8.18.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@nomicfoundation/edr": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/edr-darwin-arm64": "0.11.3", + "@nomicfoundation/edr-darwin-x64": "0.11.3", + "@nomicfoundation/edr-linux-arm64-gnu": "0.11.3", + "@nomicfoundation/edr-linux-arm64-musl": "0.11.3", + "@nomicfoundation/edr-linux-x64-gnu": "0.11.3", + "@nomicfoundation/edr-linux-x64-musl": "0.11.3", + "@nomicfoundation/edr-win32-x64-msvc": "0.11.3" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-darwin-arm64": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-darwin-x64": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-musl": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-gnu": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-musl": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-win32-x64-msvc": { + "version": "0.11.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/hardhat-ethers": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "lodash.isequal": "^4.5.0" + }, + "peerDependencies": { + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-foundry": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-network-helpers": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ethereumjs-util": "^7.1.4" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "optionalDependencies": { + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.2", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.2" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.1.9", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.1.9", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sentry/core": { + "version": "5.30.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/hub": { + "version": "5.30.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/minimal": { + "version": "5.30.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/node": { + "version": "5.30.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/core": "5.30.0", + "@sentry/hub": "5.30.0", + "@sentry/tracing": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/tracing": { + "version": "5.30.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/tracing/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@typechain/ethers-v6": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", + "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "ethers": "6.x", + "typechain": "^8.3.2", + "typescript": ">=4.7.0" + } + }, + "node_modules/@typechain/hardhat": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-9.1.0.tgz", + "integrity": "sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0" + }, + "peerDependencies": { + "@typechain/ethers-v6": "^0.5.1", + "ethers": "^6.1.0", + "hardhat": "^2.9.9", + "typechain": "^8.3.2" + } + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pbkdf2": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/secp256k1": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@uniswap/permit2-sdk": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "ethers": "^5.7.0", + "tiny-invariant": "^1.1.0" + } + }, + "node_modules/@uniswap/permit2-sdk/node_modules/ethers": { + "version": "5.8.0", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/@uniswap/sdk-core": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-7.7.2.tgz", + "integrity": "sha512-0KqXw+y0opBo6eoPAEoLHEkNpOu0NG9gEk7GAYIGok+SHX89WlykWsRYeJKTg9tOwhLpcG9oHg8xZgQ390iOrA==", + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.0.2", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/strings": "5.7.0", + "big.js": "^5.2.2", + "decimal.js-light": "^2.5.0", + "jsbi": "^3.1.4", + "tiny-invariant": "^1.1.0", + "toformat": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/sdk-core/node_modules/@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@uniswap/sdk-core/node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.16", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.2", + "license": "MIT" + }, + "node_modules/boxen": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cipher-base": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/command-line-usage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-usage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/command-line-usage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/create-hash": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elliptic": { + "version": "6.6.1", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/enquirer": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ethereum-cryptography": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/pbkdf2": "^3.0.0", + "@types/secp256k1": "^4.0.1", + "blakejs": "^1.1.0", + "browserify-aes": "^1.2.0", + "bs58check": "^2.1.2", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "hash.js": "^1.1.7", + "keccak": "^3.0.0", + "pbkdf2": "^3.0.17", + "randombytes": "^2.1.0", + "safe-buffer": "^5.1.2", + "scrypt-js": "^3.0.0", + "secp256k1": "^4.0.1", + "setimmediate": "^1.0.5" + } + }, + "node_modules/ethereumjs-util": { + "version": "7.1.5", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ethers": { + "version": "6.15.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "license": "MIT" + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fp-ts": { + "version": "1.19.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/hardhat": { + "version": "2.26.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@ethersproject/abi": "^5.1.2", + "@nomicfoundation/edr": "^0.11.3", + "@nomicfoundation/solidity-analyzer": "^0.1.0", + "@sentry/node": "^5.18.1", + "adm-zip": "^0.4.16", + "aggregate-error": "^3.0.0", + "ansi-escapes": "^4.3.0", + "boxen": "^5.1.2", + "chokidar": "^4.0.0", + "ci-info": "^2.0.0", + "debug": "^4.1.1", + "enquirer": "^2.3.0", + "env-paths": "^2.2.0", + "ethereum-cryptography": "^1.0.3", + "find-up": "^5.0.0", + "fp-ts": "1.19.3", + "fs-extra": "^7.0.1", + "immutable": "^4.0.0-rc.12", + "io-ts": "1.10.4", + "json-stream-stringify": "^3.1.4", + "keccak": "^3.0.2", + "lodash": "^4.17.11", + "micro-eth-signer": "^0.14.0", + "mnemonist": "^0.38.0", + "mocha": "^10.0.0", + "p-map": "^4.0.0", + "picocolors": "^1.1.0", + "raw-body": "^2.4.1", + "resolve": "1.17.0", + "semver": "^6.3.0", + "solc": "0.8.26", + "source-map-support": "^0.5.13", + "stacktrace-parser": "^0.1.10", + "tinyglobby": "^0.2.6", + "tsort": "0.0.1", + "undici": "^5.14.0", + "uuid": "^8.3.2", + "ws": "^7.4.6" + }, + "bin": { + "hardhat": "internal/cli/bootstrap.js" + }, + "peerDependencies": { + "ts-node": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/hardhat/node_modules/@noble/hashes": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/hardhat/node_modules/@scure/base": { + "version": "1.1.9", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat/node_modules/@scure/bip32": { + "version": "1.1.5", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/@scure/bip39": { + "version": "1.1.1", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "node_modules/hardhat/node_modules/fs-extra": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/hardhat/node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/hardhat/node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/hardhat/node_modules/ws": { + "version": "7.5.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "dev": true, + "license": "MIT" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/io-ts": { + "version": "1.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "fp-ts": "^1.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbi": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", + "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==", + "license": "Apache-2.0" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stream-stringify": { + "version": "3.1.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=7.10.1" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru_map": { + "version": "0.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/micro-eth-signer": { + "version": "0.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "micro-packed": "~0.7.2" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/curves": { + "version": "1.8.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.2" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/hashes": { + "version": "1.7.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-packed": { + "version": "0.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mnemonist": { + "version": "0.38.5", + "dev": true, + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.0" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/pbkdf2/node_modules/create-hash": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/pbkdf2/node_modules/hash-base": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/ripemd160": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.17.0", + "dev": true, + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/rlp": { + "version": "2.2.7", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "bn.js": "^5.2.0" + }, + "bin": { + "rlp": "bin/rlp" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/secp256k1": { + "version": "4.0.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/solady": { + "version": "0.1.26", + "license": "MIT" + }, + "node_modules/solc": { + "version": "0.8.26", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true, + "license": "WTFPL OR MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toformat": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/toformat/-/toformat-2.0.0.tgz", + "integrity": "sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" + }, + "bin": { + "write-markdown": "dist/write-markdown.js" + } + }, + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=3.7.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "license": "0BSD" + }, + "node_modules/tsort": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/type-detect": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typechain": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" + } + }, + "node_modules/typechain/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/typechain/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typechain/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/typechain/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typechain/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "license": "MIT", + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..b903d31b --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "twap-auction-e2e", + "version": "1.0.0", + "description": "End-to-end test suite for TWAP Auction system", + "main": "index.js", + "scripts": { + "test": "hardhat test", + "test:setup": "hardhat test --grep 'Setup'", + "test:interaction": "hardhat test --grep 'Interaction'", + "test:combined": "hardhat test --grep 'Combined'", + "test:all": "hardhat test", + "e2e": "forge build && npm run typechain && hardhat test test/e2e/tests/e2e.test.ts", + "e2e:run": "forge build && npm run typechain && npx ts-node test/e2e/src/E2ECliRunner.ts", + "e2e:shell": "forge build && npm run typechain && ./test/e2e/run-e2e-tests.sh", + "typechain": "hardhat typechain", + "compile": "hardhat compile", + "clean": "hardhat clean", + "build": "tsc", + "build:watch": "tsc --watch", + "type-check": "tsc --noEmit", + "lint": "tsc --noEmit" + }, + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "@nomicfoundation/hardhat-foundry": "^1.2.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.0", + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.0", + "@types/node": "^20.0.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "chai": "^4.3.7", + "hardhat": "^2.19.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.0", + "typechain": "^8.3.2", + "typescript": "^5.0.0" + }, + "dependencies": { + "@uniswap/permit2-sdk": "^1.4.0", + "@uniswap/sdk-core": "^7.7.2", + "ethers": "^6.8.0", + "solady": "^0.1.26" + } +} diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 00000000..7a818e40 --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,567 @@ +# TWAP Auction E2E Test Suite + +This directory contains the end-to-end (E2E) test suite for the TWAP Auction system. The test suite allows one to define complex auction scenarios using TypeScript interfaces and validate the entire auction lifecycle from deployment to completion. + +## โš ๏ธ Important Notes + +- **Auction Start Block**: Cannot be block 1. Must be block 2 or later. +- **Interaction Timing**: Interactions can begin on the same block as the auction start block, but not before + +## โ“ Frequently Asked Questions + +### How can I check events? + +You can check events by defining an assertion with the `EVENT` type: + +```typescript +{ + atBlock: 50, + reason: "Check event emission", + assert: { + type: AssertionInterfaceType.EVENT, + eventName: "BidSubmitted", + expectedArgs: { + bidder: "0x1111111111111111111111111111111111111111", + amount: "90000000000000000000000000000000" + } + } +} +``` + +**How it works:** + +- Events emitted on-chain are reconstructed from the contract ABI +- Each event is formatted as `"EventName(arg1,arg2,arg3)"` +- The test checks if the event name and arguments match your assertion +- `expectedArgs` is optional - keys are for logging clarity only +- You can use the exact event format: `"EventName(arg1,arg2,arg3)"` as the `eventName` + +### How can I check for expected reverts? + +For any action you wish to check a revert on, add the `expectRevert` field: + +```typescript +{ + type: ActionType.TRANSFER_ACTION, + interactions: [ + [ + { + atBlock: 200, + value: { + from: "0x4444444444444444444444444444444444444444" as Address, + to: "0x2222222222222222222222222222222222222222" as Address, + token: "USDC", + amount: "1000000", // 1 USDC + expectRevert: "ERC20InsufficientBalance" + } + } + ] + ] +} +``` + +**How it works:** + +- On-chain reverts are decoded using the contract ABI +- Reverts are formatted as `"ExampleRevert(arg1,arg2,arg3)"` +- The test checks if the revert contains your `expectRevert` string +- You can use just the revert name or include arguments + +### How can I add variance to checkpoint fields? + +You can add variance to any checkpoint field to accommodate bid variance by using the `VariableAmount` structure: + +```typescript +{ + atBlock: 50, + reason: "Check auction state with variance", + assert: { + type: AssertionInterfaceType.AUCTION, + currencyRaised: { + amount: "1000000000000000000000", // Expected amount + variation: "5%" // 5% variance allowed + }, + clearingPrice: { + amount: "79228162514264337593543950336000", // Expected price + variation: "0.1" // 10% variance allowed (decimal format) + } + } +} +``` + +**How it works:** + +- Use `amount` for the expected value +- Use `variation` for the allowed variance (supports both percentage "5%" and decimal "0.05" formats) +- The test checks if the actual value is within the variance bounds +- Useful for accommodating bid variance and MEV scenarios +- Works with all auction state fields: `currencyRaised`, `clearingPrice`, `isGraduated` +- Also works with checkpoint fields like `totalCleared`, `totalBids`, etc. + +## โœจ Key Features + +- **๐ŸŽฏ Targeted Testing** - Run only compatible setup/interaction combinations +- **โšก Same-Block Execution** - Multiple transactions in the same block with query priority +- **๐Ÿ“Š Comprehensive Logging** - Detailed execution traces and state information +- **๐Ÿ› ๏ธ Flexible Interface** - npm scripts, command-line options, and shell scripts +- **๐Ÿ” Real Contract Integration** - Uses actual Foundry auction contracts, not mocks +- **๐Ÿ“‹ Type Safety** - TypeScript interfaces for compile-time validation and IDE support +- **๐ŸŽฎ MEV Testing** - Test transaction ordering and arbitrage scenarios + +## ๐Ÿ—๏ธ Architecture + +The E2E test suite is built on top of Hardhat and consists of several key components: + +### Core Components + +- **`src/SchemaValidator.ts`** - Loads and validates TypeScript instance files +- **`src/AuctionDeployer.ts`** - Deploys auction contracts and sets up the environment +- **`src/BidSimulator.ts`** - Simulates bids and interactions with the auction +- **`src/AssertionEngine.ts`** - Validates checkpoints and assertions +- **`src/SingleTestRunner.ts`** - Orchestrates the complete test execution +- **`src/MultiTestRunner.ts`** - Manages multiple test combinations +- **`src/E2ECliRunner.ts`** - Command-line interface for running specific combinations +- **`src/constants.ts`** - Centralized configuration and error messages + +### Test Data + +- **`instances/setup/`** - TypeScript files defining auction setup parameters +- **`instances/interaction/`** - TypeScript files defining bid scenarios and interactions +- **`schemas/`** - TypeScript interface definitions for type safety + +## ๐Ÿ”„ Execution Flow + +The test suite follows a structured execution flow: + +1. **๐Ÿ“‹ Type Validation** - Validate setup and interaction TypeScript files +2. **๐Ÿ—๏ธ Environment Setup** - Deploy auction contracts and configure tokens +3. **โš–๏ธ Balance Setup** - Set initial balances for test accounts +4. **๐Ÿ“… Event Scheduling** - Collect and sort all events (bids, actions, checkpoints) by block +5. **๐Ÿ”€ Block Grouping** - Group events by block number for same-block execution +6. **๐ŸŽฏ Execution** - Execute events block by block with query priority +7. **โœ… Validation** - Validate checkpoints and assertions +8. **๐Ÿ“Š Reporting** - Generate test results and final state + +### Same-Block Execution Details + +- **Block Mining**: Only mines once per block, then executes all events in that block +- **Query Priority**: Checkpoints/queries execute before transactions in the same block +- **Transaction Ordering**: Maintains original order for same-type events +- **State Preservation**: All state changes are preserved between transactions + +## ๐Ÿš€ Quick Start + +### Prerequisites + +- Node.js (v18+ recommended) +- npm or yarn +- Hardhat installed globally or locally + +### Running Tests + +```bash +# From the project root directory +npm run e2e + +# Or run directly with Hardhat +npx hardhat test test/e2e/tests/e2e.test.ts + +# Or use the shell script +./test/e2e/run-e2e-tests.sh +``` + +### Available Scripts + +- `npm run e2e` - Run the main E2E test suite +- `npm run e2e:run` - Run predefined setup/interaction combinations +- `npm run e2e:shell` - Run using the shell script with compilation + +### Command-Line Options + +```bash +# Run predefined combinations +npm run e2e:run + +# Run specific combination +npx ts-node test/e2e/src/E2ECliRunner.ts --setup SimpleSetup.ts --interaction SimpleInteraction.ts + +# Show help +npx ts-node test/e2e/src/E2ECliRunner.ts --help + +# Verbose output +./test/e2e/run-e2e-tests.sh --verbose +``` + +## ๐Ÿ“‹ Test Structure + +The E2E test suite now uses **TypeScript interfaces** instead of JSON schemas for better type safety, IDE support, and maintainability. + +### Benefits of TypeScript Schemas + +- **Type Safety**: Compile-time validation of test data structure with strict `Address` type (42-char hex strings) +- **IDE Support**: Auto-completion, refactoring, and error detection +- **Maintainability**: Easier to update and extend test scenarios +- **Runtime Validation**: Built-in type guards for runtime checks +- **Documentation**: Self-documenting interfaces with clear type definitions +- **Address Validation**: Enforces proper Ethereum address format (0x + 40 hex digits) + +### Address Type + +The E2E test suite uses a strict `Address` type that ensures all addresses are properly formatted: + +```typescript +export type Address = `0x${string}` & { readonly length: 42 }; +``` + +This type: + +- **Enforces 0x prefix**: Must start with `0x` +- **Enforces exact length**: Must be exactly 42 characters (0x + 40 hex digits) +- **Provides compile-time safety**: TypeScript will catch invalid addresses at compile time +- **Supports type assertions**: Use `as Address` for string literals + +**Examples:** + +```typescript +// โœ… Valid addresses +"0x1111111111111111111111111111111111111111" as Address; +"0x0000000000000000000000000000000000000000" as Address; + +// โŒ Invalid addresses (TypeScript errors) +("0x111"); // Too short +("1111111111111111111111111111111111111111"); // Missing 0x +("0x11111111111111111111111111111111111111111"); // Too long +``` + +### Setup Schema + +The setup schema defines the auction environment using TypeScript interfaces: + +```typescript +import { TestSetupData, Address } from '../schemas/TestSetupSchema'; + +export const simpleSetup: TestSetupData = { + env: { + chainId: 31337, + startBlock: "2", + balances: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + amount: "1000000000000000000000" + } + ] + }, + auctionParameters: { + currency: "0x0000000000000000000000000000000000000000" as Address, + auctionedToken: "SimpleToken", + tokensRecipient: "0x2222222222222222222222222222222222222222" as Address, + fundsRecipient: "0x3333333333333333333333333333333333333333" as Address, + auctionDurationBlocks: 50, + claimDelayBlocks: 10, + tickSpacing: "792281625142643375935439503360", + validationHook: "0x0000000000000000000000000000000000000000" as Address, + floorPrice: "79228162514264337593543950336000", + auctionStepsData: [ + { + mpsPerBlock: 100, + blockDelta: 100, + }, + ... + ], + }, + additionalTokens: [ + { + name: "SimpleToken", + decimals: "18", + totalSupply: "1000000000000000000000000", + percentAuctioned: "10" + } + ] +}; +``` + +### Interaction Schema + +The interaction schema defines bid scenarios and assertions using TypeScript interfaces: + +```typescript +import { TestInteractionData, Address, AssertionInterfaceType } from "../schemas/TestInteractionSchema"; + +export const simpleInteraction: TestInteractionData = { + namedBidders: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + label: "SimpleBidder", + bids: [ + { + atBlock: 10, + amount: { side: "input", type: "raw", value: "1000000000000000000" }, + price: { type: "raw", value: "87150978765690771352898345369600" }, + expectRevert: "InsufficientBalance", + }, + ], + recurringBids: [], + }, + ], + assertions: [ + { + atBlock: 20, + reason: "Check bidder balance after auction", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + expected: "0", + }, + }, + ], +}; +``` + +## ๐Ÿ”ง Configuration + +### Hardhat Configuration + +The E2E tests use the main project's Hardhat configuration with the following key settings: + +- **Solidity version**: 0.8.26 +- **Optimizer**: Enabled with 200 runs +- **viaIR**: Enabled for complex contracts +- **Networks**: Hardhat local network with 20 test accounts + +### Contract Integration + +The test suite integrates with the real Foundry auction contracts: + +- **`AuctionFactory`** - Deploys new auction instances +- **`Auction`** - The main auction contract +- **`WorkingCustomMockToken`** - Custom mock ERC20 tokens for testing +- **`USDCMock`** - Mock USDC token with 6 decimals + +The test suite loads contract artifacts directly from Foundry's `out` directory, ensuring compatibility with the latest compiled contracts. The `forge build` command is automatically run before tests to ensure artifacts are up to date. + +## ๐Ÿงช Writing Tests + +### Creating a New Setup + +1. Create a new TypeScript file in `instances/setup/` (e.g., `MySetup.ts`) +2. Import the `TestSetupData` and `Address` types from the schema +3. Define the auction parameters and environment with proper type annotations +4. Use `as Address` type assertions for 42-character hex addresses +5. Specify additional tokens to deploy +6. Set up initial balances for test accounts + +### Creating a New Interaction + +1. Create a new TypeScript file in `instances/interaction/` (e.g., `MyInteraction.ts`) +2. Import the `TestInteractionData` and `Address` types from the schema +3. Define bid scenarios with specific blocks and amounts +4. Add assertions (formerly checkpoints) to validate auction state +5. Use proper tick pricing for bid amounts +6. Use `as Address` type assertions for addresses + +### Same-Block Transactions + +The test suite supports multiple transactions in the same block, which is useful for testing: + +- **MEV scenarios** - Transaction ordering and arbitrage opportunities +- **Gas optimization** - Multiple transactions hitting block gas limits +- **Real-world behavior** - True same-block execution like Ethereum + +```typescript +{ + namedBidders: [ + { + address: "0x111..." as Address, + bids: [{ atBlock: 10, ... }] // Same block + }, + { + address: "0x222..." as Address, + bids: [{ atBlock: 10, ... }] // Same block + } + ], + assertions: [ + { + atBlock: 10, // Same block - executes FIRST (queries before transactions) + assert: {...} + } + ] +} +``` + +**Execution Order**: Queries/assertions execute before transactions in the same block. + +### Expected Reverts + +You can test that certain operations should fail by adding an `expectRevert` field to your bids: + +```typescript +{ + atBlock: 10, + amount: { side: "input", type: "raw", value: "1000000000000000000" }, + price: { type: "raw", value: "87150978765690771352898345369600" }, + expectRevert: "InsufficientBalance" +} +``` + +The `expectRevert` field accepts a string that should be contained in the revert data. The test will pass if the transaction reverts and the revert data contains the specified string. + +### Adding Assertions + +Assertions (formerly checkpoints) allow you to validate auction state at specific blocks. The E2E test suite supports multiple assertion types: + +#### Available Assertion Types + +- **`AssertionInterfaceType.BALANCE`** - Validate token or native currency balances +- **`AssertionInterfaceType.TOTAL_SUPPLY`** - Validate total supply of tokens +- **`AssertionInterfaceType.EVENT`** - Validate that specific events were emitted + +#### Balance Assertions + +```typescript +{ + atBlock: 20, + reason: "Validate auction state", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, // or token name + expected: "0" + } +} +``` + +#### Total Supply Assertions + +```typescript +{ + atBlock: 20, + reason: "Validate token supply", + assert: { + type: AssertionInterfaceType.TOTAL_SUPPLY, + token: "SimpleToken", // or token address + expected: "1000000000000000000000000" + } +} +``` + +#### Event Assertions + +```typescript +{ + atBlock: 20, + reason: "Validate event emission", + assert: { + type: AssertionInterfaceType.EVENT, + eventName: "BidPlaced", + expectedArgs: { + bidder: "0x1111111111111111111111111111111111111111", + amount: "1000000000000000000" + } + } +} +``` + +## ๐Ÿ› Debugging + +### Common Issues + +1. **Tick Price Validation Errors** - Ensure floor price and tick spacing match the Foundry tests +2. **Balance Assertion Failures** - Check that expected balances account for bid amounts +3. **Contract Deployment Issues** - Verify that all required contracts are properly imported +4. **Expected Revert Failures** - Ensure the `expectRevert` string matches the actual revert data +5. **Native Currency Issues** - Use `0x0000000000000000000000000000000000000000` for native currency +6. **TypeScript Type Errors** - Use `as Address` type assertions for 42-character hex addresses +7. **Export Name Mismatches** - Ensure export names match the expected patterns (camelCase for PascalCase filenames) + +### Enhanced Error Handling + +The test suite now features centralized error messages and enhanced debugging: + +- **๐ŸŽฏ Centralized Error Messages** - All error messages are defined in `src/constants.ts` for consistency +- **๐Ÿ“Š Structured Error Context** - Error messages include relevant debugging informationtroubleshooting + +### Debug Output + +The test suite provides detailed logging: + +- ๐Ÿ—๏ธ Auction deployment progress +- ๐Ÿ” Bid execution details with same-block grouping +- ๐Ÿ’ฐ Balance validation results +- โœ… Assertion validation results +- ๐Ÿ“Š Final auction state +- ๐Ÿ“… Block-by-block event execution + +### Verbose Mode + +Run tests with verbose output for more detailed information: + +```bash +npx hardhat test test/e2e/tests/e2e.test.ts --verbose +``` + +## ๐Ÿ“ File Structure + +``` +test/e2e/ +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ src/ # Core test components +โ”‚ โ”œโ”€โ”€ SchemaValidator.ts # TypeScript instance loading +โ”‚ โ”œโ”€โ”€ AuctionDeployer.ts # Contract deployment +โ”‚ โ”œโ”€โ”€ BidSimulator.ts # Bid simulation +โ”‚ โ”œโ”€โ”€ AssertionEngine.ts # Assertion validation +โ”‚ โ”œโ”€โ”€ SingleTestRunner.ts # Test orchestration +โ”‚ โ”œโ”€โ”€ MultiTestRunner.ts # Multi-test management +โ”‚ โ”œโ”€โ”€ E2ECliRunner.ts # CLI interface +โ”‚ โ”œโ”€โ”€ constants.ts # Centralized configuration and error messages +โ”‚ โ””โ”€โ”€ types.ts # TypeScript type definitions +โ”œโ”€โ”€ instances/ # Test data +โ”‚ โ”œโ”€โ”€ setup/ # Auction setup TypeScript files +โ”‚ โ”‚ โ””โ”€โ”€ SimpleSetup.ts +โ”‚ โ””โ”€โ”€ interaction/ # Interaction TypeScript files +โ”‚ โ””โ”€โ”€ SimpleInteraction.ts +โ”œโ”€โ”€ schemas/ # TypeScript interface definitions +โ”‚ โ”œโ”€โ”€ TestSetupSchema.ts # Setup data interfaces with Address type +โ”‚ โ””โ”€โ”€ TestInteractionSchema.ts # Interaction data interfaces with Address type +โ”œโ”€โ”€ tests/ # Test files +โ”‚ โ””โ”€โ”€ e2e.test.ts # Main E2E test +โ”œโ”€โ”€ run-e2e-tests.sh # Shell script for running tests +โ”œโ”€โ”€ artifacts/ # Compiled contracts +โ””โ”€โ”€ cache/ # Hardhat cache +``` + +## ๐Ÿค Contributing + +When adding new test scenarios: + +1. Follow the existing TypeScript interface patterns +2. Use descriptive names for setup and interaction files (`.ts` extension) +3. Import the appropriate interfaces from `schemas/` directory +4. Use `as Address` type assertions for 42-character hex addresses +5. Add appropriate assertions to validate expected behavior +6. Test with both native currency (0x000...000) and ERC20 token currencies +7. Document any new assertion types or validation logic +8. Test same-block scenarios for MEV and gas optimization +9. Use the command-line interface for targeted testing +10. Ensure export names follow camelCase convention (e.g., `simpleSetup` for `SimpleSetup.ts`) + +### Adding New Combinations + +To add new test combinations, edit `test/e2e/src/E2ECliRunner.ts`: + +```typescript +const COMBINATIONS_TO_RUN = [ + { setup: "SimpleSetup.ts", interaction: "SimpleInteraction.ts" }, + { setup: "MySetup.ts", interaction: "MyInteraction.ts" }, // Add new combinations + // Only add compatible combinations! +]; +``` + +**Important**: Only add combinations that are compatible (same tokens, currencies, etc.). + +## ๐Ÿ“š Related Documentation + +- [Foundry Test Documentation](../test/README.md) +- [Auction Contract Interface](../../src/interfaces/IAuction.sol) +- [Hardhat Configuration](../../hardhat.config.ts) diff --git a/test/e2e/instances/interaction/AdvancedInteraction.ts b/test/e2e/instances/interaction/AdvancedInteraction.ts new file mode 100644 index 00000000..e87a3b0d --- /dev/null +++ b/test/e2e/instances/interaction/AdvancedInteraction.ts @@ -0,0 +1,192 @@ +import { + TestInteractionData, + Address, + AssertionInterfaceType, + PriceType, + ActionType, + AdminActionMethod, +} from "../../schemas/TestInteractionSchema"; + +export const advancedInteraction: TestInteractionData = { + name: "AdvancedInteraction", + namedBidders: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + label: "AdvancedBidder", + recurringBids: [], + bids: [ + { + atBlock: 20, + amount: { value: "1000000000000000000" }, // 1 ETH + price: { type: PriceType.RAW, value: "87150978765690771352898345369600" }, + prevTickPrice: "79228162514264337593543950336000", + }, + { + atBlock: 50, + amount: { value: "500000000000000000" }, // 0.5 ETH + price: { type: PriceType.RAW, value: "95073795017117205112252740403200" }, + prevTickPrice: "79228162514264337593543950336000", + }, + ], + }, + { + address: "0x3333333333333333333333333333333333333333" as Address, + label: "RecurringBidder", + recurringBids: [ + { + startBlock: 55, + intervalBlocks: 10, + occurrences: 5, + amount: { value: "200000000000000000" }, // 0.2 ETH + price: { type: PriceType.RAW, value: "102996611268543638871607135436800" }, + previousTick: 1, + previousTickIncrement: 1, + amountFactor: 1.0, // 0% increase each time + priceFactor: 1.0, // 0% increase each time + hookData: "0x", + }, + ], + bids: [], + }, + ], + actions: [ + { + type: ActionType.TRANSFER_ACTION, + interactions: [ + [ + { + atBlock: 80, + value: { + from: "0x1111111111111111111111111111111111111111" as Address, + to: "0x2222222222222222222222222222222222222222" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, // Native ETH + amount: "1000000000000000000", // 1 ETH + }, + }, + ], + ], + }, + { + type: ActionType.ADMIN_ACTION, + interactions: [ + [ + { + atBlock: 172, + method: AdminActionMethod.SWEEP_CURRENCY, + }, + ], + ], + }, + { + type: ActionType.TRANSFER_ACTION, + interactions: [ + [ + { + atBlock: 200, + value: { + from: "0x4444444444444444444444444444444444444444" as Address, + to: "0x2222222222222222222222222222222222222222" as Address, + token: "USDC", + amount: "1000000", // 1 USDC + expectRevert: "ERC20InsufficientBalance", + }, + }, + ], + ], + }, + { + type: ActionType.ADMIN_ACTION, + interactions: [ + [ + { + atBlock: 201, + method: AdminActionMethod.CHECKPOINT, + }, + ], + ], + }, + ], + assertions: [ + { + atBlock: 50, + reason: "Check event emission", + assert: { + type: AssertionInterfaceType.EVENT, + eventName: "BidSubmitted", + // expectedArgs removed entirely - just check the event name + }, + }, + { + atBlock: 51, + reason: "Check balance of AdvancedBidder", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + expected: "8500000000000000000", // 10 - 1.5 ETH + variance: "1%", // Allow some variance for gas fees + }, + }, + { + atBlock: 60, + reason: "Check recurring bidder balance after first recurring bid", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x3333333333333333333333333333333333333333" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + expected: "9800000000000000000", // 10 - 0.2 ETH + variance: "0.1%", // Allow some variance for gas fees + }, + }, + { + atBlock: 70, + reason: "Check recurring bidder balance after multiple recurring bids", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x3333333333333333333333333333333333333333" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + expected: "9600000000000000000", // 10 - 0.2 ETH - 0.2 ETH + variance: "0.5%", // Allow more variance due to multiple transactions + }, + }, + { + atBlock: 100, + reason: "Check bidder balance after complex bidding", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + expected: "7500000000000000000", // Expected ETH balance after bids and transfer (10 - 1.5 - 1 ETH transfer) + variance: "0.1%", // Allow 0.01% variance due to gas fee fluctuations + }, + }, + { + atBlock: 120, + reason: "Check total supply of auctioned token", + assert: { + type: AssertionInterfaceType.TOTAL_SUPPLY, + token: "AdvancedToken", + expected: "1000000000000000000000", // 1000 tokens total supply + }, + }, + { + atBlock: 160, + reason: "Check auction state parameters", + assert: { + type: AssertionInterfaceType.AUCTION, + isGraduated: false, + clearingPrice: "79228162514264337593543950336000", + currencyRaised: "661363636363636363", + latestCheckpoint: { + clearingPrice: "79228162514264337593543950336000", + currencyRaisedQ96_X7: "523986256628430050902756580631272727272727272726000000", + currencyRaisedAtClearingPriceQ96_X7: "0", + cumulativeMpsPerPrice: "435754893828453856764491726848000", + cumulativeMps: "5500000", + prev: "55", + next: "18446744073709551615", + }, + }, + }, + ], +}; diff --git a/test/e2e/instances/interaction/ERC20Interaction.ts b/test/e2e/instances/interaction/ERC20Interaction.ts new file mode 100644 index 00000000..5ac57578 --- /dev/null +++ b/test/e2e/instances/interaction/ERC20Interaction.ts @@ -0,0 +1,32 @@ +import { TestInteractionData, Address, AssertionInterfaceType, PriceType } from "../../schemas/TestInteractionSchema"; + +export const erc20Interaction: TestInteractionData = { + name: "ERC20Interaction", + namedBidders: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + label: "ERC20Bidder", + bids: [ + { + atBlock: 20, + amount: { value: "1000000000" }, // 1000 USDC (6 decimals) + price: { type: PriceType.RAW, value: "87150978765690771352898345369600" }, + previousTick: 1, + }, + ], + recurringBids: [], + }, + ], + assertions: [ + { + atBlock: 70, + reason: "Check bidder USDC balance after bid", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "USDC", + expected: "999000000000", // Expected USDC balance after bid (1M - 1K = 999K USDC) + }, + }, + ], +}; diff --git a/test/e2e/instances/interaction/ExtendedInteraction.ts b/test/e2e/instances/interaction/ExtendedInteraction.ts new file mode 100644 index 00000000..ffb93dfa --- /dev/null +++ b/test/e2e/instances/interaction/ExtendedInteraction.ts @@ -0,0 +1,260 @@ +import { + TestInteractionData, + Address, + PriceType, + ActionType, + AdminActionMethod, + AssertionInterfaceType, +} from "../../schemas/TestInteractionSchema"; + +/** + * Complex Extended interaction with extended bidding patterns + * + * Supply schedule: 150M tokens total + * - Block 100,810-100,811: 33% (49.5M tokens) @ floor = ~4,331 ETH needed + * - Block 108,011-108,012: 5% (7.5M tokens) @ floor = ~656 ETH + * - Block 115,212-115,213: 5% (7.5M tokens) @ floor = ~656 ETH + * - Block 122,413-122,414: 10% (15M tokens) @ floor = ~1,312 ETH + * - Block 129,614-129,615: 10% (15M tokens) @ floor = ~1,312 ETH + * - Block 136,815-136,816: 10% (15M tokens) @ floor = ~1,312 ETH + * - Block 144,016-144,017: 27% (40.5M tokens) @ floor = ~3,544 ETH + * + * Floor price: 0.0000875 ETH (6932464219998130000000000 in Q96) + * Tick spacing: 69324642199981300000000 + * Tick prices: floor + (n * tickSpacing) where n = 0, 1, 2, 3... + */ +export const extendedInteraction: TestInteractionData = { + name: "ExtendedInteraction", + namedBidders: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + label: "EarlyBidder", + recurringBids: [ + { + startBlock: 511, + intervalBlocks: 7, + occurrences: 10, + amount: { value: "500000000000000000000" }, // 500 ETH at floor + price: { + type: PriceType.RAW, + value: "7695035284197924300000000", // Tick 100 - floor price + }, + prevTickPrice: "6932464219998130000000000", + }, + ], + bids: [ + { + atBlock: 811, // Right after 33% release - need ~4,331 ETH for 33% + amount: { value: "5000000000000000000000" }, // 5,000 ETH at floor + price: { + type: PriceType.RAW, + value: "7695035284197924300000000", // Tick 100 - floor price + }, + prevTickPrice: "6932464219998130000000000", // Use floor price as hint + }, + { + atBlock: 812, // Continued bidding + amount: { value: "3000000000000000000000" }, // 3,000 ETH + price: { + type: PriceType.RAW, + value: "7625710641997943000000000", // Tick 110 - above floor + }, + prevTickPrice: "6932464219998130000000000", + }, + { + atBlock: 108012, // Right after first 5% release + amount: { value: "5000000000000000000000" }, // 2,000 ETH at floor + price: { + type: PriceType.RAW, + value: "13726279155596297400000000", // Floor price + }, + prevTickPrice: "6932464219998130000000000", + }, + ], + }, + { + address: "0x2222222222222222222222222222222222222222" as Address, + label: "AggressiveBidder", + recurringBids: [ + { + startBlock: 115213, // Starting at second 5% release + intervalBlocks: 7201, // Bid right after each release phase + occurrences: 4, // Bid at 4 release phases (115213, 122414, 129615, 136816) + amount: { value: "2500000000000000000000" }, // 2,500 ETH per bid = 10,000 ETH total + price: { + type: PriceType.RAW, + value: "13864928439996260000000000", + }, + priceFactor: 1.01, + prevTickPrice: "6932464219998130000000000", + hookData: "0x", + }, + ], + bids: [], + }, + { + address: "0x5555555555555555555555555555555555555555" as Address, + label: "SmallSingleBidder", + recurringBids: [], + bids: [ + { + atBlock: 100101, + amount: { value: "1000000000000000000" }, // 1 ETH + price: { + type: PriceType.RAW, + value: "9012203485997569000000000", // Floor price + }, + prevTickPrice: "6932464219998130000000000", + }, + ], + }, + { + address: "0x3333333333333333333333333333333333333333" as Address, + label: "ConservativeBidder", + recurringBids: [ + { + startBlock: 100100, + intervalBlocks: 2, + occurrences: 3, + amount: { value: "3000000000000000000" }, // 3 ETH + price: { + type: PriceType.RAW, + value: "6932464219998130000000000", // Floor price + }, + prevTickPrice: "6932464219998130000000000", + }, + ], + bids: [ + { + atBlock: 108050, // During wait + amount: { value: "3000000000000000000000" }, // 3,000 ETH + price: { + type: PriceType.RAW, + value: "8318957063997756000000000", // Tick 120 - higher price + }, + prevTickPrice: "6932464219998130000000000", // Hint at tick 110 + }, + { + atBlock: 115250, // During wait + amount: { value: "3000000000000000000000" }, // 3,000 ETH + price: { + type: PriceType.RAW, + value: "9012203485997569000000000", // Tick 130 + }, + prevTickPrice: "6932464219998130000000000", // Hint at tick 120 + }, + ], + }, + { + address: "0x4444444444444444444444444444444444444444" as Address, + label: "LateEntrant", + recurringBids: [ + { + startBlock: 130007, + intervalBlocks: 1, + occurrences: 4, + amount: { value: "4000000000000000000" }, // 4 ETH + price: { + type: PriceType.RAW, + value: "9705449907997382000000000", // Tick 140 + }, + prevTickPrice: "6932464219998130000000000", + }, + ], + bids: [ + { + atBlock: 130000, // Mid-late auction + amount: { value: "4000000000000000000000" }, // 4,000 ETH + price: { + type: PriceType.RAW, + value: "9705449907997382000000000", // Tick 140 + }, + prevTickPrice: "6932464219998130000000000", // Hint at tick 130 + }, + { + atBlock: 140001, // Late in auction + amount: { value: "4000000000000000000000" }, // 4,000 ETH + price: { + type: PriceType.RAW, + value: "17400485192195306300000000", + }, + prevTickPrice: "6932464219998130000000000", // Hint at tick 140 + }, + { + atBlock: 140002, // Late in auction + amount: { value: "4000000000000000000000" }, // 4,000 ETH + price: { + type: PriceType.RAW, + value: "17469809834395287600000000", + }, + prevTickPrice: "6932464219998130000000000", // Hint at tick 140 + }, + { + atBlock: 140004, // Late in auction + amount: { value: "4000000000000000000000" }, // 4,000 ETH + price: { + type: PriceType.RAW, + value: "18024406971995138000000000", + }, + prevTickPrice: "6932464219998130000000000", // Hint at tick 140 + }, + { + atBlock: 144016, // Final 27% release block + amount: { value: "8000000000000000000000" }, // 8,000 ETH + price: { + type: PriceType.RAW, + value: "18024406971995138000000000", + }, + prevTickPrice: "6932464219998130000000000", + }, + ], + }, + ], + groups: [ + { + labelPrefix: "FirstGroup", + startBlock: 100, + amount: { value: "500000000000000000" }, + price: { type: PriceType.RAW, value: "7209762788798055200000000" }, + prevTickPrice: "6932464219998130000000000", + rotationIntervalBlocks: 1, + betweenRoundStartsBlocks: 32, + rounds: 9, + hookData: "0x", + }, + { + labelPrefix: "PopulousGroup", + startBlock: 100102, + amount: { value: "1000000000000000000", variation: "1%" }, + price: { type: PriceType.RAW, value: "9012203485997569000000000" }, + prevTickPrice: "6932464219998130000000000", + rotationIntervalBlocks: 2, + betweenRoundStartsBlocks: 0, + rounds: 1, + }, + { + labelPrefix: "BandwagonGroup", + startBlock: 100403, + amount: { value: "1000000000000000000", variation: "10%" }, + price: { type: PriceType.RAW, value: "9012203485997569000000000" }, + prevTickPrice: "6932464219998130000000000", + rotationIntervalBlocks: 2, + betweenRoundStartsBlocks: 0, + rounds: 1, + }, + ], + actions: [ + { + type: ActionType.ADMIN_ACTION, + interactions: [ + [ + { + atBlock: 144017, + method: AdminActionMethod.CHECKPOINT, + }, + ], + ], + }, + ], + assertions: [], +}; diff --git a/test/e2e/instances/interaction/SimpleInteraction.ts b/test/e2e/instances/interaction/SimpleInteraction.ts new file mode 100644 index 00000000..5cbe3b3b --- /dev/null +++ b/test/e2e/instances/interaction/SimpleInteraction.ts @@ -0,0 +1,35 @@ +import { TestInteractionData, Address, AssertionInterfaceType, PriceType } from "../../schemas/TestInteractionSchema"; + +export const simpleInteraction: TestInteractionData = { + name: "SimpleInteraction", + namedBidders: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + label: "SimpleBidder", + bids: [ + { + atBlock: 20, + amount: { value: "1000000000000000000" }, + price: { type: PriceType.RAW, value: "87150978765690771352898345369600" }, + previousTick: 1, + }, + ], + recurringBids: [], + }, + ], + groups: [], + actions: [], + assertions: [ + { + atBlock: 30, + reason: "Check bidder native currency balance after bid", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + expected: "999622633006543701", + variance: "0.01%", + }, + }, + ], +}; diff --git a/test/e2e/instances/interaction/VariationInteraction.ts b/test/e2e/instances/interaction/VariationInteraction.ts new file mode 100644 index 00000000..26379276 --- /dev/null +++ b/test/e2e/instances/interaction/VariationInteraction.ts @@ -0,0 +1,120 @@ +import { TestInteractionData, Address, AssertionInterfaceType, PriceType } from "../../schemas/TestInteractionSchema"; + +export const variationInteraction: TestInteractionData = { + name: "VariationInteraction", + namedBidders: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + label: "VariationBidder1", + recurringBids: [], + bids: [ + { + atBlock: 15, + // Base amount with +/- 10% variation using percentage + amount: { + value: "1000000000000000000", // 1 ETH base + variation: "10%", // +/- 10% (0.9 - 1.1 ETH) + }, + price: { type: PriceType.RAW, value: "87150978765690771352898345369600" }, + previousTick: 1, + }, + { + atBlock: 25, + // Another bid with percentage variation + amount: { + value: "500000000000000000", // 0.5 ETH base + variation: "10%", // +/- 10% (0.45 - 0.55 ETH) + }, + price: { type: PriceType.RAW, value: "90000000000000000000000000000000" }, + previousTick: 2, + }, + ], + }, + { + address: "0x2222222222222222222222222222222222222222" as Address, + label: "VariationBidder2", + recurringBids: [], + bids: [ + { + atBlock: 20, + // Bid with larger percentage variation + amount: { + value: "800000000000000000", // 0.8 ETH base + variation: "25%", // +/- 25% (0.6 - 1.0 ETH) + }, + price: { type: PriceType.RAW, value: "88150978765690771352898345369600" }, + previousTick: 1, + }, + ], + }, + ], + actions: [], + assertions: [ + { + atBlock: 30, + reason: "Check bidder 1 balance after bids with variation", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + // Expected around 3.5 ETH remaining (5 - 1 - 0.5) + // But with large variation, could be 3.35 - 3.65 ETH + expected: "4000000000000000000", // ~4 ETH remaining + variance: "5%", // Allow variance due to random amounts and gas + }, + }, + { + atBlock: 30, + reason: "Check bidder 2 balance after bid with larger variation", + assert: { + type: AssertionInterfaceType.BALANCE, + address: "0x2222222222222222222222222222222222222222" as Address, + token: "0x0000000000000000000000000000000000000000" as Address, + // Expected around 4.2 ETH remaining (5 - 0.8) + // But with large variation, could be 4.0 - 4.4 ETH + expected: "4200000000000000000", + variance: "5%", // Allow 10% variance due to large random variation + }, + }, + { + atBlock: 40, + reason: "Check total supply remains constant despite variations", + assert: { + type: AssertionInterfaceType.TOTAL_SUPPLY, + token: "VariationToken", + expected: "1000000000000000000000", // Should always be exactly this + }, + }, + { + atBlock: 45, + reason: "Check auction state with variance on currencyRaised", + assert: { + type: AssertionInterfaceType.AUCTION, + isGraduated: false, + clearingPrice: "79228162514264337593543950336000", + // Currency raised will vary based on random bid amounts + // Expected around 0.326 ETH with variance due to bid variations + currencyRaised: { + amount: "120000000000000000", // ~0.12 ETH with corrected tick calculation + variation: "30%", // +/- 30% to account for bid variation + }, + latestCheckpoint: { + clearingPrice: "79228162514264337593543950336000", + // These will also vary, so we use VariableAmount with raw amount variation + currencyRaisedQ96_X7: { + amount: "80593446990424197784923982579080313706988320995000000", + variation: "30%", // +/- 30% to account for variation + }, + currencyRaisedAtClearingPriceQ96_X7: "0", + cumulativeMpsPerPrice: { + amount: "158456325028528675187087900672000", + variation: "11%", // +/- 50% (using percentage!) + }, + cumulativeMps: "2000000", + prev: "15", + next: "18446744073709551615", + }, + }, + }, + ], +}; diff --git a/test/e2e/instances/setup/AdvancedSetup.ts b/test/e2e/instances/setup/AdvancedSetup.ts new file mode 100644 index 00000000..5485a842 --- /dev/null +++ b/test/e2e/instances/setup/AdvancedSetup.ts @@ -0,0 +1,66 @@ +import { TestSetupData, Address } from "../../schemas/TestSetupSchema"; + +export const advancedSetup: TestSetupData = { + name: "AdvancedSetup", + env: { + chainId: 31337, + startBlock: "10", + blockTimeSec: 12, + blockGasLimit: "100000000", + txGasLimit: "30000000", + baseFeePerGasWei: "0", + fork: { + rpcUrl: "http://localhost:8545", + blockNumber: "1", + }, + balances: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "10000000000000000000", // 10 ETH for complex operations + }, + { + address: "0x2222222222222222222222222222222222222222" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "1000000000000000000", // 1 ETH + }, + { + address: "0x3333333333333333333333333333333333333333" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "10000000000000000000", // 10 ETH + }, + { + address: "0x4444444444444444444444444444444444444444" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "1000000000000000000", // 1 ETH + }, + ], + }, + + auctionParameters: { + currency: "0x0000000000000000000000000000000000000000" as Address, + auctionedToken: "AdvancedToken", + tokensRecipient: "0x3333333333333333333333333333333333333333" as Address, + fundsRecipient: "0x4444444444444444444444444444444444444444" as Address, + auctionDurationBlocks: 100, + claimDelayBlocks: 20, + tickSpacing: "396140812571321687967719751680", + validationHook: "0x0000000000000000000000000000000000000000" as Address, + floorPrice: "79228162514264337593543950336000", + }, + + additionalTokens: [ + { + name: "AdvancedToken", + decimals: "18", + totalSupply: "1000000000000000000000", // 1000 tokens + percentAuctioned: "20.0", // 20% of supply auctioned + }, + { + name: "USDC", + decimals: "6", + totalSupply: "1000000000000", + percentAuctioned: "0.0", + }, + ], +}; diff --git a/test/e2e/instances/setup/ERC20Setup.ts b/test/e2e/instances/setup/ERC20Setup.ts new file mode 100644 index 00000000..6527e0a8 --- /dev/null +++ b/test/e2e/instances/setup/ERC20Setup.ts @@ -0,0 +1,56 @@ +import { TestSetupData, Address } from "../../schemas/TestSetupSchema"; + +export const erc20Setup: TestSetupData = { + name: "ERC20Setup", + env: { + chainId: 31337, + startBlock: "10", + blockTimeSec: 12, + blockGasLimit: "30000000", + txGasLimit: "30000000", + baseFeePerGasWei: "0", + fork: { + rpcUrl: "http://localhost:8545", + blockNumber: "1", + }, + balances: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000", // Native ETH for gas + amount: "2000000000000000000", // 2 ETH for gas fees + }, + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "USDC", + amount: "1000000000000", // 1M USDC (6 decimals) + }, + ], + }, + + auctionParameters: { + currency: "USDC", // Use USDC as currency instead of native ETH + auctionedToken: "SimpleToken", + tokensRecipient: "0x2222222222222222222222222222222222222222" as Address, + fundsRecipient: "0x3333333333333333333333333333333333333333" as Address, + auctionDurationBlocks: 50, + claimDelayBlocks: 10, + tickSpacing: 100, + validationHook: "0x0000000000000000000000000000000000000000" as Address, + floorPrice: "79228162514264337593543950336000", + }, + + additionalTokens: [ + { + name: "SimpleToken", + decimals: "18", + totalSupply: "1000000000000000000000", + percentAuctioned: "10.0", + }, + { + name: "USDC", + decimals: "6", + totalSupply: "1000000000000", + percentAuctioned: "0.0", + }, + ], +}; diff --git a/test/e2e/instances/setup/ExtendedSetup.ts b/test/e2e/instances/setup/ExtendedSetup.ts new file mode 100644 index 00000000..46055a81 --- /dev/null +++ b/test/e2e/instances/setup/ExtendedSetup.ts @@ -0,0 +1,122 @@ +import { TestSetupData, Address } from "../../schemas/TestSetupSchema"; + +/** + * Extended Testing Parameters Setup + * + * Based on ExtendedTestingParams.xlsx: + * - ETH price: $4,000 + * - Floor price: $350M FDV โ†’ $0.35/token โ†’ 0.0000875 ETH + * - Total supply: 1B tokens + * - For sale: 150M tokens (15%) + * - Total auction duration: 144,007 blocks (~20 days) + * + * Supply Schedule (14 steps): + * 1. 100,800 blocks (14 days) - 0% release (wait period) + * 2. 1 block - 33% release (3.3M MPS) + * 3. 7,200 blocks (1 day) - 0% release + * 4. 1 block - 5% release (500k MPS) + * 5. 7,200 blocks (1 day) - 0% release + * 6. 1 block - 5% release (500k MPS) + * 7. 7,200 blocks (1 day) - 0% release + * 8. 1 block - 10% release (1M MPS) + * 9. 7,200 blocks (1 day) - 0% release + * 10. 1 block - 10% release (1M MPS) + * 11. 7,200 blocks (1 day) - 0% release + * 12. 1 block - 10% release (1M MPS) + * 13. 7,200 blocks (1 day) - 0% release + * 14. 1 block - 27% release (2.7M MPS) + * + * Total: 144,007 blocks, 10M MPS (100%) + * Note: The schedule is encoded in the auctionStepsData hex string below + */ +export const extendedSetup: TestSetupData = { + name: "ExtendedSetup", + env: { + chainId: 31337, + startBlock: "10", + blockTimeSec: 12, + blockGasLimit: "30000000", + txGasLimit: "30000000", + baseFeePerGasWei: "0", + fork: { + rpcUrl: "http://localhost:8545", + blockNumber: "1", + }, + + groups: [ + { + labelPrefix: "FirstGroup", + count: 8, + startNativeEach: "100000000000000000000000", + startAmountEach: "100000000000000000000000", + }, + { + labelPrefix: "PopulousGroup", + count: 297, + startNativeEach: "10000000000000000000", + }, + { + labelPrefix: "BandwagonGroup", + count: 483, + startNativeEach: "10000000000000000000", + }, + ], + balances: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "100000000000000000000000", // 100,000 ETH + }, + { + address: "0x2222222222222222222222222222222222222222" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "100000000000000000000000", // 100,000 ETH + }, + { + address: "0x3333333333333333333333333333333333333333" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "100000000000000000000000", // 100,000 ETH + }, + { + address: "0x4444444444444444444444444444444444444444" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "100000000000000000000000", // 100,000 ETH + }, + { + address: "0x5555555555555555555555555555555555555555" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "100000000000000000000000", // 100,000 ETH + }, + ], + }, + + auctionParameters: { + currency: "0x0000000000000000000000000000000000000000" as Address, // Native ETH + auctionedToken: "ExtendedToken", + tokensRecipient: "0x3333333333333333333333333333333333333333" as Address, + fundsRecipient: "0x4444444444444444444444444444444444444444" as Address, + auctionDurationBlocks: 144007, // Total duration from decoded steps data + claimDelayBlocks: 7200, // 1 day + tickSpacing: "69324642199981300000000", // From params - large value as string + validationHook: "0x0000000000000000000000000000000000000000" as Address, + floorPrice: "6932464219998130000000000", // 0.0000875 ETH in Q96 format + // Using the exact hex string from the params file + auctionStepsData: + "0x00000000000189c0325aa000000000010000000000001c2007a12000000000010000000000001c2007a12000000000010000000000001c200f424000000000010000000000001c200f424000000000010000000000001c200f424000000000010000000000001c202932e00000000001", + }, + + additionalTokens: [ + { + name: "ExtendedToken", + decimals: "18", + totalSupply: "1000000000000000000000000000", // 1 billion tokens (1e9 * 1e18) + percentAuctioned: "15.0", // 15% of 1B = 150M tokens + }, + { + name: "USDC", + decimals: "6", + totalSupply: "1000000000000", + percentAuctioned: "0.0", + }, + ], +}; diff --git a/test/e2e/instances/setup/SimpleSetup.ts b/test/e2e/instances/setup/SimpleSetup.ts new file mode 100644 index 00000000..77def4e5 --- /dev/null +++ b/test/e2e/instances/setup/SimpleSetup.ts @@ -0,0 +1,51 @@ +import { TestSetupData, Address } from "../../schemas/TestSetupSchema"; + +export const simpleSetup: TestSetupData = { + name: "SimpleSetup", + env: { + chainId: 31337, + startBlock: "10", + blockTimeSec: 12, + blockGasLimit: "30000000", + txGasLimit: "30000000", + baseFeePerGasWei: "0", + fork: { + rpcUrl: "http://localhost:8545", + blockNumber: "1", + }, + balances: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "2000000000000000000", + }, + ], + }, + + auctionParameters: { + currency: "0x0000000000000000000000000000000000000000" as Address, + auctionedToken: "SimpleToken", + tokensRecipient: "0x2222222222222222222222222222222222222222" as Address, + fundsRecipient: "0x3333333333333333333333333333333333333333" as Address, + auctionDurationBlocks: 50, + claimDelayBlocks: 10, + tickSpacing: 100, + validationHook: "0x0000000000000000000000000000000000000000" as Address, + floorPrice: "79228162514264337593543950336000", + }, + + additionalTokens: [ + { + name: "SimpleToken", + decimals: "18", + totalSupply: "1000000000000000000000", + percentAuctioned: "10.0", + }, + { + name: "USDC", + decimals: "6", + totalSupply: "1000000000000", + percentAuctioned: "0.0", + }, + ], +}; diff --git a/test/e2e/instances/setup/VariationSetup.ts b/test/e2e/instances/setup/VariationSetup.ts new file mode 100644 index 00000000..24b85ca7 --- /dev/null +++ b/test/e2e/instances/setup/VariationSetup.ts @@ -0,0 +1,56 @@ +import { TestSetupData, Address } from "../../schemas/TestSetupSchema"; + +export const variationSetup: TestSetupData = { + name: "VariationSetup", + env: { + chainId: 31337, + startBlock: "10", + blockTimeSec: 12, + blockGasLimit: "30000000", + txGasLimit: "30000000", + baseFeePerGasWei: "0", + fork: { + rpcUrl: "http://localhost:8545", + blockNumber: "1", + }, + balances: [ + { + address: "0x1111111111111111111111111111111111111111" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "5000000000000000000", // 5 ETH + }, + { + address: "0x2222222222222222222222222222222222222222" as Address, + token: "0x0000000000000000000000000000000000000000", + amount: "5000000000000000000", // 5 ETH + }, + ], + }, + + auctionParameters: { + currency: "0x0000000000000000000000000000000000000000" as Address, + auctionedToken: "VariationToken", + tokensRecipient: "0x3333333333333333333333333333333333333333" as Address, + fundsRecipient: "0x4444444444444444444444444444444444444444" as Address, + auctionDurationBlocks: 50, + claimDelayBlocks: 10, + tickSpacing: 100, + validationHook: "0x0000000000000000000000000000000000000000" as Address, + floorPrice: "79228162514264337593543950336000", + }, + + additionalTokens: [ + { + name: "VariationToken", + decimals: "18", + totalSupply: "1000000000000000000000", + percentAuctioned: "10.0", + }, + { + name: "USDC", + decimals: "6", + totalSupply: "1000000000000", + percentAuctioned: "0.0", + }, + ], +}; diff --git a/test/e2e/run-e2e-tests.sh b/test/e2e/run-e2e-tests.sh new file mode 100755 index 00000000..a2294b7a --- /dev/null +++ b/test/e2e/run-e2e-tests.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# E2E Test Runner Script +# Runs all setup + interaction combinations + +set -e # Exit on any error + +echo "๐Ÿงช TWAP Auction E2E Test Suite" +echo "================================" + +# Check if we're in the right directory (can be run from project root or test/e2e/) +if [ -f "package.json" ]; then + # Running from project root + SCRIPT_DIR="test/e2e" +elif [ -f "../../package.json" ]; then + # Running from test/e2e directory + SCRIPT_DIR="." +else + echo "โŒ Error: Please run this script from the project root or test/e2e directory" + exit 1 +fi + +# Install dependencies if needed +if [ ! -d "node_modules" ]; then + echo "๐Ÿ“ฆ Installing dependencies..." + npm install +fi + +# Compile contracts +echo "๐Ÿ”จ Compiling contracts..." +npx hardhat compile + +# Run the combined tests +echo "๐Ÿš€ Running E2E tests..." +echo "" + +# Run all combined tests +npx hardhat test test/e2e/tests/e2e.test.ts + +echo "" +echo "โœ… E2E tests completed!" + +# Optional: Run with verbose output +if [ "$1" = "--verbose" ]; then + echo "" + echo "๐Ÿ” Running with verbose output..." + npx hardhat test test/e2e/tests/e2e.test.ts --verbose +fi + +# Optional: Run specific combination +if [ "$1" = "--setup" ] && [ "$2" ] && [ "$3" = "--interaction" ] && [ "$4" ]; then + echo "" + echo "๐ŸŽฏ Running specific combination: $2 + $4" + npx hardhat test test/e2e/tests/e2e.test.ts --grep "Should run $2 \\+ $4 combination" +fi diff --git a/test/e2e/schemas/TestInteractionSchema.ts b/test/e2e/schemas/TestInteractionSchema.ts new file mode 100644 index 00000000..1e4febc8 --- /dev/null +++ b/test/e2e/schemas/TestInteractionSchema.ts @@ -0,0 +1,176 @@ +// TypeScript interfaces for test interaction schema +export type Address = `0x${string}` & { readonly length: 42 }; + +export enum AssertionInterfaceType { + BALANCE = "balance", + TOTAL_SUPPLY = "totalSupply", + EVENT = "event", + AUCTION = "auction", + REVERT = "revert", +} + +export enum ActionType { + ADMIN_ACTION = "AdminAction", + TRANSFER_ACTION = "TransferAction", +} + +export enum PriceType { + RAW = "raw", + TICK = "tick", +} + +export enum AdminActionMethod { + CHECKPOINT = "checkpoint", + SWEEP_CURRENCY = "sweepCurrency", + SWEEP_UNSOLD_TOKENS = "sweepUnsoldTokens", +} + +export interface AmountConfig { + value: string; + variation?: string; + token?: Address | string; +} + +export interface PriceConfig { + type: PriceType; + value: string; + variation?: string; +} + +export interface BidData { + atBlock: number; + amount: AmountConfig; + price: PriceConfig; + previousTick?: number; + prevTickPrice?: string; // Optional: directly specify the price hint instead of calculating from previousTick + hookData?: string; + expectRevert?: string; +} + +export interface RecurringBid { + startBlock: number; + intervalBlocks: number; + occurrences: number; + amount: AmountConfig; + price: PriceConfig; + amountFactor?: number; + priceFactor?: number; + hookData?: string; + previousTick?: number; + prevTickPrice?: string; // Optional: directly specify the price hint + previousTickIncrement?: number; + prevTickPriceIncrement?: number | string; // Support large values as string +} + +export interface NamedBidder { + address: Address; + label?: string; + bids: BidData[]; + recurringBids: RecurringBid[]; +} + +export interface Group { + labelPrefix: string; + startBlock: number; + amount: AmountConfig; + price: PriceConfig; + previousTick?: number; + prevTickPrice?: string; + rotationIntervalBlocks: number; + betweenRoundStartsBlocks: number; + rounds: number; + hookData?: string; +} + +export interface AdminAction { + atBlock: number; + method: AdminActionMethod; +} + +export interface TransferAction { + atBlock: number; + value: { + from: Address; + to: Address | string; + token: Address | string; + amount: string; + expectRevert?: string; + }; +} + +export interface BalanceAssertion { + type: AssertionInterfaceType.BALANCE; + address: Address; + token: Address | string; + expected: string; + variance?: string; // Optional variance field. Can be a ratio (e.g., "0.05") or percentage (e.g., "5%") +} + +export interface TotalSupplyAssertion { + type: AssertionInterfaceType.TOTAL_SUPPLY; + token: Address | string; + expected: string; +} + +export interface EventAssertion { + type: AssertionInterfaceType.EVENT; + eventName: string; + expectedArgs?: Record; // Optional - if not provided, just checks event name +} + +export interface RevertAssertion { + type: AssertionInterfaceType.REVERT; + expected: string; +} + +export interface AuctionAssertion { + type: AssertionInterfaceType.AUCTION; + isGraduated: boolean; + clearingPrice: string | VariableAmount; + currencyRaised: string | VariableAmount; + latestCheckpoint: InternalCheckpointStruct; +} + +export interface InternalCheckpointStruct { + clearingPrice: string | VariableAmount; + currencyRaisedQ96_X7: string | VariableAmount; + currencyRaisedAtClearingPriceQ96_X7: string | VariableAmount; + cumulativeMpsPerPrice: string | VariableAmount; + cumulativeMps: string | VariableAmount; + prev: string | VariableAmount; + next: string | VariableAmount; +} + +export interface VariableAmount { + amount: string; + variation: string; +} + +export type Assertion = BalanceAssertion | TotalSupplyAssertion | EventAssertion | AuctionAssertion; + +export interface AssertionInfo { + atBlock: number; + reason: string; + assert: Assertion; +} + +export interface TestInteractionData { + name: string; + namedBidders?: NamedBidder[]; + groups?: Group[]; + actions?: Array<{ + type: ActionType; + interactions: Array; + }>; + assertions?: AssertionInfo[]; +} + +// Type guards for runtime validation + +export function isValidPriceType(type: string): type is PriceConfig["type"] { + return Object.values(PriceType).includes(type as PriceType); +} + +export function isValidAdminActionKind(kind: string): kind is AdminAction["method"] { + return Object.values(AdminActionMethod).includes(kind as AdminActionMethod); +} diff --git a/test/e2e/schemas/TestSetupSchema.ts b/test/e2e/schemas/TestSetupSchema.ts new file mode 100644 index 00000000..b4723d21 --- /dev/null +++ b/test/e2e/schemas/TestSetupSchema.ts @@ -0,0 +1,87 @@ +// TypeScript interfaces for test setup schema +import { ChainId } from "@uniswap/sdk-core"; + +export type Address = `0x${string}` & { readonly length: 42 }; + +export interface ForkConfig { + rpcUrl: string; + blockNumber: string; +} + +export interface BalanceItem { + address: Address; // Address of the account to set the balance for + token: Address | string; + amount: string; +} + +export interface StepData { + mpsPerBlock: number; // Milli-per-second (actually per-block) rate for this step + blockDelta: number; // Number of blocks this step lasts +} + +export interface GroupConfig { + labelPrefix: string; + count: number; + startAmountEach?: string; + startNativeEach?: string; + addresses?: Address[]; +} + +export interface Environment { + chainId?: ChainId | 31337; // 31337 is local Hardhat network + startBlock: string; + offsetBlocks?: number; + blockTimeSec?: number; + blockGasLimit?: string; + txGasLimit?: string; + baseFeePerGasWei?: string; + fork?: ForkConfig; + balances?: BalanceItem[]; + groups?: GroupConfig[]; +} + +export interface AuctionParameters { + currency: Address | string; + auctionedToken: Address | string; + tokensRecipient: Address; + fundsRecipient: Address; + auctionDurationBlocks: number; + claimDelayBlocks: number; + tickSpacing: number | bigint | string; // number for small values, bigint/string for large values + validationHook: Address; + floorPrice: string; + auctionStepsData?: string | StepData[]; // Optional: raw hex string or array of steps +} + +export interface AdditionalToken { + name: string; + decimals: string; + totalSupply: string; + percentAuctioned: string; +} + +export interface TestSetupData { + name: string; + env: Environment; + auctionParameters: AuctionParameters; + additionalTokens: AdditionalToken[]; +} + +// Type guards for runtime validation +export function isValidChainId(chainId: number): chainId is ChainId | 31337 { + return Object.values(ChainId).includes(chainId as ChainId) || chainId === 31337; +} + +export function isValidAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/.test(address); +} + +export function isValidUint64(value: string): boolean { + const num = BigInt(value); + return num >= 0n && num <= 0xffffffffffffffffn; +} + +export function isValidUint256(value: string): boolean { + const num = BigInt(value); + return num >= 0n && num <= 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffn; +} diff --git a/test/e2e/src/AssertionEngine.ts b/test/e2e/src/AssertionEngine.ts new file mode 100644 index 00000000..426abb6a --- /dev/null +++ b/test/e2e/src/AssertionEngine.ts @@ -0,0 +1,409 @@ +import { + AssertionInterfaceType, + BalanceAssertion, + AuctionAssertion, + EventAssertion, + TotalSupplyAssertion, + Assertion, + Address, + VariableAmount, +} from "../schemas/TestInteractionSchema"; +import { Contract } from "ethers"; +import { TokenContract } from "./types"; +import { AuctionDeployer } from "./AuctionDeployer"; +import { ZERO_ADDRESS, LOG_PREFIXES, ERROR_MESSAGES, TYPES, TYPE_FIELD } from "./constants"; +import { CheckpointStruct } from "../../../typechain-types/out/Auction"; +import { resolveTokenAddress } from "./utils"; +import hre from "hardhat"; + +// NOTE: Different from interface defined in the schema as it uses bigint, and no variance, since this comes directly from the contract +export interface AuctionState { + isGraduated: boolean; + clearingPrice: bigint; + currencyRaised: bigint; + latestCheckpoint: CheckpointStruct; + currentBlock: number; +} + +export interface BidderState { + address: Address; + tokenBalance: string; + currencyBalance: string; +} + +export class AssertionEngine { + private auction: Contract; + private token: TokenContract | null; // Contract or Native currency + private currency: TokenContract | null; // Contract or Native currency + private auctionDeployer: AuctionDeployer; + + constructor( + auction: Contract, + token: TokenContract | null, + currency: TokenContract | null, + auctionDeployer: AuctionDeployer, + ) { + this.auction = auction; + this.token = token; + this.currency = currency; + this.auctionDeployer = auctionDeployer; + } + + /** + * Validates a single assertion by routing to the appropriate validation method. + * @param assertion - The assertion to validate + * @throws Error if the assertion fails validation + */ + async validateAssertion(assertion: Assertion): Promise { + if (assertion.type === AssertionInterfaceType.BALANCE) { + await this.validateBalanceAssertion(assertion); + } else if (assertion.type === AssertionInterfaceType.TOTAL_SUPPLY) { + await this.validateTotalSupplyAssertion([assertion]); + } else if (assertion.type === AssertionInterfaceType.EVENT) { + await this.validateEventAssertion([assertion]); + } else if (assertion.type === AssertionInterfaceType.AUCTION) { + await this.validateAuctionAssertion([assertion]); + } + } + + /** + * Check if the actual value is within the specified variance of the expected value. + * Supports three formats: + * - Percentage: "5%" -> 5% tolerance + * - Ratio: "0.05" -> 5% tolerance (decimal < 1) + * - Raw amount: "100000000000000000" -> +/- exact amount tolerance + * @param actual - The actual value + * @param expected - The expected value + * @param varianceStr - The variance string (percentage, ratio, or raw amount) + * @returns True if actual is within variance bounds, false otherwise + */ + private isWithinVariance(actual: bigint, expected: bigint, varianceStr: string): boolean { + if (!varianceStr) { + return actual === expected; + } + + let varianceAmount: bigint; + + if (varianceStr.endsWith("%")) { + // Percentage: convert to ratio and apply to expected (e.g., "5%" -> 5% of expected) + const percentage = parseFloat(varianceStr.slice(0, -1)); + const ratio = percentage / 100; + varianceAmount = (expected * BigInt(Math.floor(ratio * 1000000))) / 1000000n; + } else { + if (varianceStr.includes(".")) { + const numericValue = parseFloat(varianceStr); + // Ratio: treat as percentage in decimal form (e.g., "0.05" -> 5% of expected) + varianceAmount = (expected * BigInt(Math.floor(numericValue * 1000000))) / 1000000n; + } else { + // Raw amount: use as absolute tolerance (e.g., "100000000000000000") + varianceAmount = BigInt(varianceStr); + } + } + + const lowerBound = expected - varianceAmount; + const upperBound = expected + varianceAmount; + + return actual >= lowerBound && actual <= upperBound; + } + + /** + * Validates equality between expected and actual values, supporting VariableAmount structures. + * @param expected - Expected value (can be primitive or VariableAmount object) + * @param actual - Actual value from the contract + * @returns True if values are equal or within variance bounds + * @throws Error if expected is an invalid object structure + */ + private validateEquality(expected: any, actual: any): boolean { + if (expected && typeof expected === TYPES.OBJECT) { + let keys = Object.keys(expected); + if (keys.length !== 2 || !keys.includes("amount") || !keys.includes("variation")) { + throw new Error(ERROR_MESSAGES.CANNOT_VALIDATE_EQUALITY); + } + let expectedStruct = expected as VariableAmount; + if (!this.isWithinVariance(actual, BigInt(expectedStruct.amount), expectedStruct.variation)) { + return false; + } else { + return true; + } + } else { + return expected.toString() === actual.toString(); + } + } + + /** + * Validates a balance assertion for a specific address and token. + * @param assertion - Balance assertion containing address, token, expected balance, and optional variance + * @throws Error if balance assertion fails or token is not found + */ + async validateBalanceAssertion(assertion: BalanceAssertion): Promise { + const { address, token, expected, variance } = assertion; + + let actualBalance: bigint; + const expectedBalance = BigInt(expected); + + const resolvedTokenAddress = await resolveTokenAddress(token, this.auctionDeployer); + + if (resolvedTokenAddress === ZERO_ADDRESS) { + // Native currency + actualBalance = await hre.ethers.provider.getBalance(address); + } else { + // ERC20 token + const tokenContract = await hre.ethers.getContractAt("IERC20Minimal", resolvedTokenAddress); + actualBalance = await tokenContract.balanceOf(address); + } + if (!this.isWithinVariance(actualBalance, expectedBalance, variance || "0")) { + throw new Error( + ERROR_MESSAGES.BALANCE_ASSERTION_FAILED( + address, + token, + expectedBalance.toString(), + actualBalance.toString(), + variance, + ), + ); + } + // Log actual balance for visibility + const tokenSymbol = resolvedTokenAddress === ZERO_ADDRESS ? "ETH" : "tokens"; + console.log( + LOG_PREFIXES.SUCCESS, + `Assertion validated (within variance of ${variance}). Actual balance: ${actualBalance.toString()} ${tokenSymbol}`, + ); + } + + /** + * Validates total supply assertions for multiple tokens. + * @param totalSupplyAssertion - Array of total supply assertions to validate + * @throws Error if any total supply assertion fails or token is not found + */ + async validateTotalSupplyAssertion(totalSupplyAssertion: TotalSupplyAssertion[]): Promise { + for (const assertion of totalSupplyAssertion) { + const tokenAddress = await resolveTokenAddress(assertion.token, this.auctionDeployer); + const token = await this.auctionDeployer.getTokenByAddress(tokenAddress); + + if (!token) { + throw new Error(ERROR_MESSAGES.TOKEN_NOT_FOUND_BY_ADDRESS(tokenAddress)); + } + const _addr = await token.getAddress(); + console.log(LOG_PREFIXES.INFO, "Token address for totalSupply():", _addr); + const actualSupply = await token.totalSupply(); + const expectedSupply = BigInt(assertion.expected); + + if (actualSupply !== expectedSupply) { + throw new Error( + ERROR_MESSAGES.TOTAL_SUPPLY_ASSERTION_FAILED(expectedSupply.toString(), actualSupply.toString()), + ); + } + + console.log( + LOG_PREFIXES.ASSERTION, + "Total supply check:", + tokenAddress, + "has", + actualSupply.toString(), + "total supply, expected", + expectedSupply.toString(), + ); + } + } + + /** + * Validates auction state assertions including main auction fields and checkpoint data. + * @param auctionAssertions - Array of auction assertions, optionally including variances, to validate + * @throws Error if any auction assertion fails + */ + async validateAuctionAssertion(auctionAssertions: AuctionAssertion[]): Promise { + for (const assertion of auctionAssertions) { + console.log(LOG_PREFIXES.INFO, "Auction assertion validation"); + + // Get the current auction state + const auctionState = await this.getAuctionState(); + + for (const key of Object.keys(assertion)) { + if (key === TYPE_FIELD) continue; + if (key === "latestCheckpoint") continue; + let expected = assertion[key as keyof AuctionAssertion]; + if (expected != undefined && expected != null) { + if (!this.validateEquality(expected, auctionState[key as keyof AuctionState])) { + // Check if this is a VariableAmount with variance + const variance = typeof expected === "object" && "variation" in expected ? expected.variation : undefined; + throw new Error( + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED( + typeof expected === "object" && "amount" in expected + ? expected.amount + : assertion[key as keyof AuctionAssertion], + auctionState[key as keyof AuctionState], + key, + variance, + ), + ); + } + } + } + + if (assertion.latestCheckpoint) { + for (const key of Object.keys(assertion.latestCheckpoint)) { + if (key === TYPE_FIELD) continue; + let expected = assertion.latestCheckpoint[key as keyof CheckpointStruct]; + if (expected != undefined && expected != null) { + if (!this.validateEquality(expected, auctionState.latestCheckpoint[key as keyof CheckpointStruct])) { + // Check if this is a VariableAmount with variance + const variance = typeof expected === "object" && "variation" in expected ? expected.variation : undefined; + throw new Error( + ERROR_MESSAGES.AUCTION_CHECKPOINT_ASSERTION_FAILED( + typeof expected === "object" && "amount" in expected + ? expected.amount + : assertion.latestCheckpoint[key as keyof CheckpointStruct], + auctionState.latestCheckpoint[key as keyof CheckpointStruct], + key, + variance, + ), + ); + } + } + } + } + const { type, ...assertionWithoutType } = assertion; + console.log( + LOG_PREFIXES.SUCCESS, + "Auction state check successful. Expected", + assertionWithoutType, + "got", + auctionState, + ); + } + } + + /** + * Validates event assertions by checking if specific events were emitted in the current block. + * @param eventAssertion - Array of event assertions to validate + * @throws Error if any event assertion fails or block is not found + */ + async validateEventAssertion(eventAssertion: EventAssertion[]): Promise { + for (const assertion of eventAssertion) { + console.log(LOG_PREFIXES.INFO, "Event assertion validation for event:", assertion.eventName); + + // Get the current block to check for events + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const block = await hre.ethers.provider.getBlock(currentBlock); + + if (!block) { + throw new Error(ERROR_MESSAGES.BLOCK_NOT_FOUND(currentBlock)); + } + + const eventFound = await this.checkForEventInBlock(block, assertion); + + if (!eventFound) { + throw new Error(ERROR_MESSAGES.EVENT_ASSERTION_FAILED(assertion.eventName)); + } + + console.log(LOG_PREFIXES.SUCCESS, "Event assertion validated:", assertion.eventName); + } + } + + /** + * Checks if a specific event was emitted in a given block. + * @param block - The block to search for events + * @param assertion - Event assertion containing event name and expected arguments + * @returns True if the event is found with matching arguments, false otherwise + */ + private async checkForEventInBlock(block: any, assertion: EventAssertion): Promise { + // Check all transactions in the block for the event + console.log(LOG_PREFIXES.INFO, "Checking for event in block:", block.number); + for (const txHash of block.transactions) { + const receipt = await hre.ethers.provider.getTransactionReceipt(txHash); + + if (!receipt) continue; + // Check each log in the transaction + for (const log of receipt.logs) { + try { + // Try to decode the log using the auction contract interface + const parsedLog = this.auction.interface.parseLog({ + topics: log.topics, + data: log.data, + }); + if (parsedLog && parsedLog.name === assertion.eventName) { + // Check if the event arguments match expected values + const matches = this.checkEventArguments(parsedLog.args, assertion.expectedArgs); + if (matches) { + return true; + } + } + if (parsedLog) { + let joinedEvent = parsedLog.name + "(" + parsedLog.args.join(",") + ")"; + if (joinedEvent.toLowerCase().includes(assertion.eventName.toLowerCase())) { + return true; + } + } + } catch (error) { + // Log parsing failed, continue to next log + continue; + } + } + } + + return false; + } + + /** + * Checks if event arguments match the expected values. + * @param actualArgs - Array of actual event arguments from the contract + * @param expectedArgs - Object containing expected argument values + * @returns True if all expected arguments are found in the actual arguments, false otherwise + */ + private checkEventArguments(actualArgs: any, expectedArgs?: Record): boolean { + if (!expectedArgs || Object.keys(expectedArgs).length === 0) { + return true; // No args to check, just event name match is enough + } + for (const [, expectedValue] of Object.entries(expectedArgs)) { + let contains = false; + softMatchLoop: for (let i = 0; i < actualArgs.length; i++) { + if (actualArgs[i].toString() == expectedValue.toString()) { + contains = true; + break softMatchLoop; + } + } + if (!contains) { + return false; + } + } + return true; + } + + /** + * Retrieves the current auction state from the contract. + * @returns AuctionState object containing isGraduated, clearingPrice, currencyRaised, latestCheckpoint, and currentBlock + */ + async getAuctionState(): Promise { + const [isGraduated, clearingPrice, currencyRaised, latestCheckpoint, currentBlock] = await Promise.all([ + this.auction.isGraduated(), + this.auction.clearingPrice(), + this.auction.currencyRaised(), + this.auction.latestCheckpoint(), + hre.ethers.provider.getBlockNumber(), + ]); + + return { + isGraduated, + clearingPrice, + currencyRaised, + latestCheckpoint, + currentBlock, + }; + } + + /** + * Retrieves the current state of a bidder including token and currency balances. + * @param bidderAddress - The address of the bidder to check + * @returns BidderState object containing address, tokenBalance, and currencyBalance + */ + async getBidderState(bidderAddress: Address): Promise { + const tokenBalance = this.token ? await this.token.balanceOf(bidderAddress) : 0n; + const currencyBalance = this.currency ? await this.currency.balanceOf(bidderAddress) : 0n; + + return { + address: bidderAddress, + tokenBalance: tokenBalance.toString(), + currencyBalance: currencyBalance.toString(), + }; + } +} diff --git a/test/e2e/src/AuctionDeployer.ts b/test/e2e/src/AuctionDeployer.ts new file mode 100644 index 00000000..326f7740 --- /dev/null +++ b/test/e2e/src/AuctionDeployer.ts @@ -0,0 +1,707 @@ +import { TestSetupData, Address, AdditionalToken, StepData } from "../schemas/TestSetupSchema"; +import mockTokenArtifact from "../../../out/WorkingCustomMockToken.sol/WorkingCustomMockToken.json"; +import auctionArtifact from "../../../out/Auction.sol/Auction.json"; +import auctionFactoryArtifact from "../../../out/AuctionFactory.sol/AuctionFactory.json"; +import { AuctionParametersStruct } from "../../../typechain-types/out/Auction"; +import { TransactionInfo } from "./types"; +import { + NATIVE_CURRENCY_ADDRESS, + MPS, + MAX_SYMBOL_LENGTH, + HEX_PADDING_LENGTH, + DEFAULT_TOTAL_SUPPLY, + ERROR_MESSAGES, + LOG_PREFIXES, + METHODS, + PENDING_STATE, + ZERO_ADDRESS, +} from "./constants"; +import { AuctionContract, TokenContract, AuctionFactoryContract, AuctionConfig } from "./types"; +import hre from "hardhat"; + +export class AuctionDeployer { + private ethers: typeof hre.ethers; + private auctionFactory: AuctionFactoryContract | undefined; + private auction: AuctionContract | undefined; + private tokens: Map = new Map(); // Map of token name -> contract instance + + constructor() { + this.ethers = hre.ethers; + } + + /** + * Sets the auction factory contract reference. + * @param auctionFactory - The auction factory contract instance + */ + setAuctionFactory(auctionFactory: AuctionFactoryContract): void { + this.auctionFactory = auctionFactory; + } + /** + * Sets a token contract reference by name. + * @param name - The token name + * @param token - The token contract instance + */ + setToken(name: string, token: TokenContract): void { + this.tokens.set(name, token); + } + + /** + * Initializes the deployer by deploying tokens and setting up the auction factory. + * @param setupData - Test setup data containing auction parameters and additional tokens + * @param setupTransactions - Array to collect setup transaction information + * @throws Error if auction factory is not deployed or token deployment fails + */ + async initialize(setupData: TestSetupData, setupTransactions: TransactionInfo[]): Promise { + // Deploy auction factory + await this.deployAuctionFactory(setupTransactions, 0); + + // Deploy all tokens once + await this.deployAdditionalTokens(setupData.additionalTokens, setupTransactions, 1); + + console.log( + LOG_PREFIXES.SUCCESS, + "AuctionFactory deployed. Additional Tokens Deployer", + setupData.additionalTokens.length, + "tokens", + ); + } + + /** + * Deploys additional tokens for the test setup. + * @param additionalTokens - Array of additional token configurations + * @param setupTransactions - Array to collect setup transaction information + * @param increment - Starting increment value for deployment + */ + async deployAdditionalTokens( + additionalTokens: AdditionalToken[], + setupTransactions: TransactionInfo[], + increment: number, + ): Promise { + console.log(LOG_PREFIXES.DEPLOY, "Deploying additional tokens..."); + let i = 0; + for (const tokenConfig of additionalTokens) { + await this.deployToken(tokenConfig, setupTransactions, i + increment); + increment++; + } + } + + /** + * Deploys a single token contract. + * @param tokenConfig - Token configuration containing name, symbol, decimals, and total supply + * @param setupTransactions - Array to collect setup transaction information + * @param increment - Increment value for deployment + */ + private async deployToken( + tokenConfig: AdditionalToken, + setupTransactions: TransactionInfo[], + increment: number, + ): Promise { + // Load artifact directly from Foundry's out directory + const mockToken = await this.ethers.getContractFactory(mockTokenArtifact.abi, mockTokenArtifact.bytecode.object); + const symbol = tokenConfig.name.substring(0, Math.min(MAX_SYMBOL_LENGTH, tokenConfig.name.length)).toUpperCase(); + const decimals = parseInt(tokenConfig.decimals); + const totalSupply = tokenConfig.totalSupply || DEFAULT_TOTAL_SUPPLY; + const defaultFrom = await (await hre.ethers.getSigners())[0].getAddress(); + const from = hre.ethers.getAddress(defaultFrom); + const signer = await hre.ethers.getSigner(from); + const nonce = await signer.getNonce(PENDING_STATE); + const predicted = hre.ethers.getCreateAddress({ from, nonce: nonce + increment }); + const tx = await mockToken.getDeployTransaction(tokenConfig.name, symbol, decimals, totalSupply); + setupTransactions.push({ + tx, + from: null, + msg: "Deployed Token", + }); + const tokenContract = await hre.ethers.getContractAt("MockToken", predicted as Address); + this.tokens.set(tokenConfig.name, tokenContract as TokenContract); + return; + } + + /** + * Gets a token contract by name. + * @param tokenName - The name of the token + * @returns The token contract instance or undefined if not found + */ + getTokenByName(tokenName: string): TokenContract | undefined { + return this.tokens.get(tokenName); + } + + /** + * Gets the address of a token by name. + * @param tokenName - The name of the token + * @returns The token address or null if not found + */ + async getTokenAddress(tokenName: string): Promise
{ + const token = this.tokens.get(tokenName); + return token ? ((await token.getAddress()) as Address) : null; + } + + /** + * Gets a token contract by its address. + * @param tokenAddress - The address of the token + * @returns The token contract instance or undefined if not found + */ + async getTokenByAddress(tokenAddress: string): Promise { + // Find token by address in the tokens map + for (const [, token] of this.tokens) { + const address = await token.getAddress(); + if (address === tokenAddress) { + return token; + } + } + return undefined; + } + + /** + * Deploys the auction factory contract. + * @param setupTransactions - Array to collect setup transaction information + * @param increment - Increment value for deployment + */ + async deployAuctionFactory(setupTransactions: TransactionInfo[], increment: number): Promise { + // Load artifact directly from Foundry's out directory + const AuctionFactory = await this.ethers.getContractFactory( + auctionFactoryArtifact.abi, + auctionFactoryArtifact.bytecode.object, + ); + + const defaultFrom = await (await hre.ethers.getSigners())[0].getAddress(); + const from = hre.ethers.getAddress(defaultFrom); + const signer = await hre.ethers.getSigner(from); + const nonce = await signer.getNonce(PENDING_STATE); + const predicted = hre.ethers.getCreateAddress({ from, nonce: nonce + increment }); + const tx = await AuctionFactory.getDeployTransaction(); + setupTransactions.push({ + tx, + from: null, + msg: "Deployed AuctionFactory", + }); + const auctionFactoryContract = await hre.ethers.getContractAt("AuctionFactory", predicted as Address); + this.auctionFactory = auctionFactoryContract as AuctionFactoryContract; + return; + } + + /** + * Creates a new auction using the auction factory. + * @param setupData - Test setup data containing auction parameters + * @param setupTransactions - Array to collect setup transaction information + * @returns The deployed auction contract instance + * @throws Error if auction factory is not initialized or auction creation fails + */ + async createAuction(setupData: TestSetupData, setupTransactions: TransactionInfo[]): Promise { + if (!this.auctionFactory) { + throw new Error(ERROR_MESSAGES.AUCTION_DEPLOYER_NOT_INITIALIZED); + } + + // Get the auctioned token and currency (tokens should already be deployed) + const auctionedToken = this.getTokenByName(setupData.auctionParameters.auctionedToken); + if (!auctionedToken) { + throw new Error(ERROR_MESSAGES.AUCTIONED_TOKEN_NOT_FOUND(setupData.auctionParameters.auctionedToken)); + } + + const currencyAddress = await this.resolveCurrencyAddress(setupData.auctionParameters.currency); + + // Calculate auction parameters + const auctionConfig = this.calculateAuctionParameters(setupData, currencyAddress); + const auctionAmount = this.calculateAuctionAmount( + setupData.auctionParameters.auctionedToken, + setupData.additionalTokens, + ); + + // Log auction configuration + this.logAuctionConfiguration(auctionConfig, auctionAmount, currencyAddress, auctionedToken); + + try { + // Encode and deploy auction + const configData = this.encodeAuctionParameters(auctionConfig); + const auctionAddress = await this.deployAuctionContract( + auctionedToken, + auctionAmount, + configData, + setupTransactions, + ); + + this.auction = (await this.ethers.getContractAt(auctionArtifact.abi, auctionAddress)) as AuctionContract; + + await this.transferToAuction(auctionedToken, auctionAddress, auctionAmount, setupTransactions); + + await this.callOnTokensReceived(setupTransactions); + + return this.auction; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(LOG_PREFIXES.ERROR, "Auction creation failed:", errorMessage); + throw new Error(ERROR_MESSAGES.AUCTION_CREATION_FAILED(errorMessage)); + } + } + + async transferToAuction( + auctionedToken: TokenContract, + auctionAddress: string, + auctionAmount: bigint, + setupTransactions: TransactionInfo[], + ): Promise { + // Transfer tokens to the auction contract before calling onTokensReceived() + const transferTx = await auctionedToken.getFunction("transfer").populateTransaction(auctionAddress, auctionAmount); + setupTransactions.push({ + tx: transferTx, + from: null, + msg: `Transferred ${auctionAmount} tokens to auction`, + }); + } + + async callOnTokensReceived(setupTransactions: TransactionInfo[]): Promise { + if (!this.auction) { + throw new Error(ERROR_MESSAGES.AUCTION_NOT_DEPLOYED); + } + const onTokensReceivedTx = await this.auction.getFunction("onTokensReceived").populateTransaction(); + setupTransactions.push({ + tx: onTokensReceivedTx, + from: null, + msg: "Called onTokensReceived on auction", + }); + } + + /** + * Calculates auction parameters from setup data. + * @param setupData - Test setup data containing auction parameters and environment + * @param currencyAddress - The resolved currency address + * @returns Auction configuration object + */ + private calculateAuctionParameters(setupData: TestSetupData, currencyAddress: Address): AuctionConfig { + const { auctionParameters, env } = setupData; + + const startBlock = BigInt(env.startBlock) + BigInt(env.offsetBlocks ?? 0); + + // Ensure auction start block is always greater than 1 + if (startBlock <= 1n) { + throw new Error( + `Auction start block must be greater than 1. Calculated start block: ${startBlock.toString()}. Please increase env.startBlock or env.startOffsetBlocks.`, + ); + } + + const endBlock = startBlock + BigInt(auctionParameters.auctionDurationBlocks); + const claimBlock = endBlock + BigInt(auctionParameters.claimDelayBlocks); + + // Handle auctionStepsData: use provided value or generate simple schedule + let auctionStepsData: string; + if (auctionParameters.auctionStepsData) { + if (typeof auctionParameters.auctionStepsData === "string") { + // Raw hex string provided + auctionStepsData = auctionParameters.auctionStepsData; + } else { + // Array of StepData provided + auctionStepsData = this.createAuctionStepsDataFromArray(auctionParameters.auctionStepsData); + } + } else { + // No steps data provided, use simple linear schedule + const blockDelta = parseInt(auctionParameters.auctionDurationBlocks.toString()); + const stepData: StepData[] = [ + { + mpsPerBlock: Math.floor(MPS / blockDelta), + blockDelta: blockDelta, + }, + ]; + auctionStepsData = this.createAuctionStepsDataFromArray(stepData); + } + + return { + currency: currencyAddress, + tokensRecipient: auctionParameters.tokensRecipient, + fundsRecipient: auctionParameters.fundsRecipient, + startBlock: Number(startBlock), + endBlock: Number(endBlock), + claimBlock: Number(claimBlock), + tickSpacing: + typeof auctionParameters.tickSpacing === "string" + ? BigInt(auctionParameters.tickSpacing) + : auctionParameters.tickSpacing, + validationHook: auctionParameters.validationHook, + floorPrice: auctionParameters.floorPrice, + auctionStepsData: auctionStepsData, + }; + } + + /** + * Logs auction configuration details for debugging purposes. + * @param config - The auction configuration object + * @param auctionAmount - The amount of tokens to be auctioned + * @param currencyAddress - The address of the currency token + * @param auctionedToken - The token contract being auctioned + */ + private async logAuctionConfiguration( + config: AuctionConfig, + auctionAmount: bigint, + currencyAddress: Address, + auctionedToken: TokenContract, + ): Promise { + const currentBlock = await this.ethers.provider.getBlockNumber(); + + console.log(LOG_PREFIXES.CONFIG, "Current block number:", currentBlock); + console.log(LOG_PREFIXES.CONFIG, "Calculated auction startBlock:", config.startBlock); + console.log(LOG_PREFIXES.CONFIG, "Calculated auction endBlock:", config.endBlock); + console.log(LOG_PREFIXES.CONFIG, "Calculated auction claimBlock:", config.claimBlock); + console.log(LOG_PREFIXES.CONFIG, "Auction amount:", auctionAmount.toString()); + console.log(LOG_PREFIXES.CONFIG, "Currency address:", currencyAddress); + console.log(LOG_PREFIXES.CONFIG, "Auctioned token address:", await auctionedToken.getAddress()); + } + + /** + * Encodes auction parameters into the format expected by the contract. + * @param config - Auction configuration object + * @returns Encoded auction parameters as a hex string + * @throws Error if auction parameters type is not found in ABI + */ + private encodeAuctionParameters(config: AuctionConfig): string { + // Extract AuctionParameters struct definition from the auction artifact + const auctionParametersType = auctionArtifact.abi + .find( + (item: unknown) => + (item as any).type === "constructor" && + (item as any).inputs && + (item as any).inputs.some((input: any) => input.internalType === "struct AuctionParameters"), + ) + ?.inputs.find((input: any) => input.internalType === "struct AuctionParameters"); + + if (!auctionParametersType) { + throw new Error(ERROR_MESSAGES.AUCTION_PARAMETERS_NOT_FOUND); + } + + // Construct the tuple type string from the ABI components + const components = (auctionParametersType as any).components + .map((comp: any) => `${comp.type} ${comp.name}`) + .join(", "); + const tupleType = `tuple(${components})`; + + const auctionParameters: AuctionParametersStruct = { + currency: config.currency, + tokensRecipient: config.tokensRecipient, + fundsRecipient: config.fundsRecipient, + startBlock: config.startBlock, + endBlock: config.endBlock, + claimBlock: config.claimBlock, + tickSpacing: config.tickSpacing, + validationHook: config.validationHook, + floorPrice: config.floorPrice, + auctionStepsData: config.auctionStepsData, + }; + + const configData = this.ethers.AbiCoder.defaultAbiCoder().encode([tupleType], [auctionParameters]); + + console.log(LOG_PREFIXES.CONFIG, "Config data length:", configData.length); + return configData; + } + + /** + * Deploys the auction contract with the specified parameters. + * @param auctionedToken - The token contract to be auctioned + * @param auctionAmount - The amount of tokens to be auctioned + * @param configData - Encoded auction configuration data + * @param setupTransactions - Array to collect setup transaction information + * @returns The deployed auction contract address + * @throws Error if auction factory is not initialized or deployment fails + */ + private async deployAuctionContract( + auctionedToken: TokenContract, + auctionAmount: bigint, + configData: string, + setupTransactions: TransactionInfo[], + ): Promise { + if (!this.auctionFactory) { + throw new Error(ERROR_MESSAGES.AUCTION_FACTORY_NOT_DEPLOYED); + } + + const salt = this.ethers.keccak256(this.ethers.toUtf8Bytes("test-salt")); + + const auctionAddress = await (this.auctionFactory.initializeDistribution as any).staticCall( + await auctionedToken.getAddress(), + auctionAmount, + configData, + salt, + ); + // Generate the transaction + const tx = await this.auctionFactory + .getFunction("initializeDistribution") + .populateTransaction(await auctionedToken.getAddress(), auctionAmount, configData, salt); + const defaultFrom = await (await hre.ethers.getSigners())[0].getAddress(); + setupTransactions.push({ + tx, + from: defaultFrom, + msg: "Deployed Auction", + }); + + return auctionAddress; + } + + /** + * Resolves a currency identifier to its address. + * @param currency - Currency address or identifier + * @returns The resolved currency address + * @throws Error if currency is not found + */ + async resolveCurrencyAddress(currency: Address | string): Promise
{ + // If it's an address, return it directly + if (currency.startsWith("0x")) { + return currency as Address; + } + // Otherwise, look up the token by name + const address = await this.getTokenAddress(currency); + if (!address) { + throw new Error(ERROR_MESSAGES.TOKEN_NOT_FOUND(currency)); + } + return address; + } + + /** + * Calculates the auction amount for a given token. + * @param tokenName - The name of the token + * @param additionalTokens - Array of additional token configurations + * @returns The calculated auction amount as a bigint + * @throws Error if token configuration is not found + */ + calculateAuctionAmount(tokenName: string, additionalTokens: AdditionalToken[]): bigint { + const tokenConfig = additionalTokens.find((t) => t.name === tokenName); + if (!tokenConfig) { + throw new Error(ERROR_MESSAGES.TOKEN_NOT_FOUND(tokenName)); + } + + const totalSupply = BigInt(tokenConfig.totalSupply); + const percentAuctioned = parseFloat(tokenConfig.percentAuctioned); + return (totalSupply * BigInt(Math.floor(percentAuctioned * 100))) / BigInt(10000); + } + + /** + * Creates auction steps data from an array of StepData objects. + * @param steps - Array of step configurations with mpsPerBlock and blockDelta + * @returns Hex string of concatenated packed step data + * @throws Error if steps don't sum to correct totals + */ + createAuctionStepsDataFromArray(steps: StepData[]): string { + console.log(LOG_PREFIXES.INFO, "Creating auction steps data from array of", steps.length, "steps"); + + let hexParts: string[] = []; + let totalMps = 0; + let totalBlocks = 0; + + for (const step of steps) { + const mps = step.mpsPerBlock; + const blockDelta = step.blockDelta; + + // Pack mps (24 bits) and blockDelta (40 bits) into 8 bytes + const packed = (BigInt(mps) << 40n) | BigInt(blockDelta); + const hex = packed.toString(16).padStart(HEX_PADDING_LENGTH, "0"); + hexParts.push(hex); + + totalMps += mps * blockDelta; + totalBlocks += blockDelta; + + console.log(LOG_PREFIXES.INFO, ` Step: mps=${mps}, blockDelta=${blockDelta}, hex=0x${hex}`); + } + + console.log(LOG_PREFIXES.INFO, "Total MPS:", totalMps, "(should be", MPS + ")"); + console.log(LOG_PREFIXES.INFO, "Total blocks:", totalBlocks); + + // Validation: total mps should equal MPS constant + if (totalMps !== MPS) { + console.warn( + LOG_PREFIXES.WARNING, + `Warning: Total MPS (${totalMps}) doesn't equal required MPS (${MPS}). Auction may fail to deploy.`, + ); + } + + return "0x" + hexParts.join(""); + } + /** + * Creates simple auction steps data for a given duration. + * @param auctionDurationBlocks - The duration of the auction in blocks + * @returns The encoded auction steps data + */ + createSimpleAuctionStepsData(auctionDurationBlocks: number): string { + // Create a simple auction steps data that satisfies the validation + // Format: each step is 8 bytes (uint64): 3 bytes mps + 5 bytes blockDelta + // We need: sumMps = 1e7 (MPS constant) and sumBlockDelta = auctionDurationBlocks + + const blockDelta = parseInt(auctionDurationBlocks.toString()); + const mps = Math.floor(MPS / blockDelta); // mps * blockDelta should equal MPS + + console.log(LOG_PREFIXES.INFO, "Creating auction steps data:"); + console.log(LOG_PREFIXES.INFO, "MPS:", MPS); + console.log(LOG_PREFIXES.INFO, "blockDelta:", blockDelta); + console.log(LOG_PREFIXES.INFO, "mps:", mps); + + // Pack mps (24 bits) and blockDelta (40 bits) into 8 bytes + // mps goes in the upper 24 bits, blockDelta in the lower 40 bits + const packed = (BigInt(mps) << 40n) | BigInt(blockDelta); + + // Convert to hex string with proper padding (8 bytes = 16 hex chars) + const hex = packed.toString(16).padStart(HEX_PADDING_LENGTH, "0"); + const result = "0x" + hex; + + console.log(LOG_PREFIXES.INFO, "packed:", packed.toString()); + console.log(LOG_PREFIXES.INFO, "hex:", result); + + return result; + } + + /** + * Sets up initial balances for test environment. + * @param setupData - Test setup data containing environment configuration + * @param setupTransactions - Array to collect setup transaction information + */ + async setupBalances( + setupData: TestSetupData, + setupTransactions: TransactionInfo[], + startBalancesMap: Map>, + ): Promise { + const { env } = setupData; + if (!env.balances) return; + + console.log(LOG_PREFIXES.ASSERTION, "Setting up balances..."); + + for (const balance of env.balances) { + if (balance.token === NATIVE_CURRENCY_ADDRESS) { + await this.setupNativeCurrencyBalance(balance.address, balance.amount, startBalancesMap); + } else if (balance.token.startsWith("0x")) { + await this.setupTokenBalanceByAddress( + balance.address, + balance.token as Address, + balance.amount, + setupTransactions, + startBalancesMap, + ); + } else { + await this.setupTokenBalanceByName( + balance.address, + balance.token, + balance.amount, + setupTransactions, + startBalancesMap, + ); + } + } + + for (const group of env.groups ?? []) { + for (const address of group.addresses ?? []) { + await this.setupNativeCurrencyBalance(address, group.startNativeEach ?? "0", startBalancesMap); + const auctionCurrency = setupData.auctionParameters.currency; + if (auctionCurrency == ZERO_ADDRESS) { + await this.setupNativeCurrencyBalance(address, group.startAmountEach ?? "0", startBalancesMap); + continue; + } + if (auctionCurrency.startsWith("0x")) { + await this.setupTokenBalanceByAddress( + address, + auctionCurrency as Address, + group.startNativeEach ?? "0", + setupTransactions, + startBalancesMap, + ); + } else { + await this.setupTokenBalanceByName( + address, + auctionCurrency, + group.startNativeEach ?? "0", + setupTransactions, + startBalancesMap, + ); + } + } + } + } + + /** + * Sets up native currency balance for an address. + * @param address - The address to set the balance for + * @param amount - The amount in wei as a string + */ + private async setupNativeCurrencyBalance( + address: Address, + amount: string, + startBalancesMap: Map>, + ): Promise { + if (amount == "0") return; + const hexAmount = "0x" + BigInt(amount).toString(16); + await hre.network.provider.send(METHODS.HARDHAT.SET_BALANCE, [address, hexAmount]); + this.addToStartBalancesMap(address, NATIVE_CURRENCY_ADDRESS, BigInt(amount), startBalancesMap); + console.log(LOG_PREFIXES.SUCCESS, "Set native currency balance:", address, "=", amount, "wei"); + } + + private addToStartBalancesMap( + address: Address, + tokenAddress: Address, + amount: bigint, + startBalancesMap: Map>, + ): void { + if (!startBalancesMap.has(tokenAddress)) { + startBalancesMap.set(tokenAddress, new Map()); + } + if (!startBalancesMap.get(tokenAddress)?.has(address)) { + startBalancesMap.get(tokenAddress)?.set(address, 0n); + } + let prevBalance = startBalancesMap.get(tokenAddress)?.get(address) ?? 0n; + startBalancesMap.get(tokenAddress)?.set(address, prevBalance + BigInt(amount)); + } + /** + * Sets a token balance at bootup for an address using token address. + * @param address - The address to set the balance for + * @param tokenAddress - The token contract address + * @param amount - The amount to set + * @param setupTransactions - Array to collect setup transaction information + */ + private async setupTokenBalanceByAddress( + address: Address, + tokenAddress: Address, + amount: string, + setupTransactions: TransactionInfo[], + startBalancesMap: Map>, + ): Promise { + if (amount == "0") return; + let token: TokenContract | null = null; + for (const [, tokenContract] of this.tokens) { + if ((await tokenContract.getAddress()) === tokenAddress) { + token = tokenContract; + break; + } + } + this.addToStartBalancesMap(address, tokenAddress, BigInt(amount), startBalancesMap); + if (token) { + let tx = await token.getFunction("mint").populateTransaction(address, amount); + setupTransactions.push({ tx, from: null, msg: "Minted" }); + console.log(LOG_PREFIXES.SUCCESS, "Minted", amount, "tokens to", address, "(", await token.getAddress(), ")"); + } else { + console.warn(LOG_PREFIXES.WARNING, "Token not found for address:", tokenAddress); + } + } + + /** + * Sets a token balance at bootup for an address using token name. + * @param address - The address to set the balance for + * @param tokenName - The name of the token + * @param amount - The amount to set + * @param setupTransactions - Array to collect setup transaction information + */ + private async setupTokenBalanceByName( + address: Address, + tokenName: string, + amount: string, + setupTransactions: TransactionInfo[], + startBalancesMap: Map>, + ): Promise { + if (amount == "0") return; + const token = this.getTokenByName(tokenName); + let tokenAddress = await token?.getAddress(); + if (tokenAddress) { + this.addToStartBalancesMap(address, tokenAddress as Address, BigInt(amount), startBalancesMap); + } + if (token && tokenAddress) { + let tx = await token.getFunction("transfer").populateTransaction(address, amount); + setupTransactions.push({ + tx, + from: null, + msg: "Minted", + }); + console.log(LOG_PREFIXES.SUCCESS, "Minted", amount, tokenName, "to", address); + } else { + console.warn(LOG_PREFIXES.WARNING, "Token not found:", tokenName); + } + } +} diff --git a/test/e2e/src/BidSimulator.ts b/test/e2e/src/BidSimulator.ts new file mode 100644 index 00000000..91529f57 --- /dev/null +++ b/test/e2e/src/BidSimulator.ts @@ -0,0 +1,481 @@ +import { TestInteractionData, Group, BidData, AdminActionMethod, AmountConfig } from "../schemas/TestInteractionSchema"; +import { Contract, ContractTransaction } from "ethers"; +import { + PERMIT2_ADDRESS, + MAX_UINT256, + ZERO_ADDRESS, + UINT_160_MAX, + UINT_48_MAX, + LOG_PREFIXES, + ERROR_MESSAGES, +} from "./constants"; +import { IAllowanceTransfer } from "../../../typechain-types/test/e2e/artifacts/permit2/src/interfaces/IAllowanceTransfer"; +import { TransactionInfo } from "./types"; +import { TransferAction, AdminAction } from "../schemas/TestInteractionSchema"; +import { calculatePrice, resolveTokenAddress, tickNumberToPriceX96 } from "./utils"; +import hre from "hardhat"; +import { TestSetupData, GroupConfig } from "../schemas/TestSetupSchema"; + +export enum BiddersType { + NAMED = "named", + GROUP = "group", +} + +export interface InternalBidData { + bidData: BidData; + bidder: string; + type: BiddersType; + group?: string; +} + +export class BidSimulator { + private auction: Contract; + private currency: Contract; + private labelMap: Map = new Map(); + private reverseLabelMap: Map = new Map(); + private groupBidders: Map = new Map(); + private auctionDeployer: any; // Add reference to auction deployer for token resolution + + constructor(auction: Contract, currency: Contract, auctionDeployer?: any) { + this.auction = auction; + this.currency = currency; + this.auctionDeployer = auctionDeployer; + } + + /** + * Sets up label mappings for symbolic addresses and group bidders. + * @param interactionData - Test interaction data containing named bidders and groups + */ + async setupLabels(interactionData: TestInteractionData, setupData: TestSetupData): Promise { + // Map symbolic labels to actual addresses + this.labelMap.set("Auction", await this.auction.getAddress()); + + // Add named bidders + if (interactionData.namedBidders) { + interactionData.namedBidders.forEach((bidder) => { + this.labelMap.set(bidder.label || bidder.address, bidder.address); + }); + } + // Generate group bidders + if (setupData.env.groups) { + await this.generateGroupBidders(setupData.env.groups); + } + } + + /** + * Generates group bidders and maps them to their group names. + * @param groups - Array of group configurations + */ + async generateGroupBidders(groups: GroupConfig[]): Promise { + for (const group of groups) { + if (!group.addresses) { + continue; + } + const bidders: string[] = []; + for (let i = 0; i < group.addresses.length; i++) { + const address = group.addresses[i]; + bidders.push(address); + this.labelMap.set(`${group.labelPrefix}-${i}`, address); + this.reverseLabelMap.set(address, `${group.labelPrefix}-${i}`); + } + this.groupBidders.set(group.labelPrefix, bidders); + } + } + + /** + * Collects all bids from named bidders and groups. + * @param interactionData - Test interaction data containing bidders and groups + * @returns Array of internal bid data for execution + */ + public collectAllBids(interactionData: TestInteractionData): InternalBidData[] { + const bids: InternalBidData[] = []; + + // Named bidders + if (interactionData.namedBidders) { + interactionData.namedBidders.forEach((bidder) => { + bidder.bids.forEach((bid) => { + const internalBid = { + bidData: bid, + bidder: bidder.address, + type: BiddersType.NAMED, + }; + bids.push(internalBid); + }); + + // Implement recurring bids support + bidder.recurringBids.forEach((recurringBid) => { + for (let i = 0; i < recurringBid.occurrences; i++) { + const blockNumber = recurringBid.startBlock + i * recurringBid.intervalBlocks; + + // Apply growth factors to amount and price + let adjustedAmount = recurringBid.amount; + let adjustedPrice = recurringBid.price; + + if (recurringBid.amountFactor && i > 0) { + const factor = Math.pow(recurringBid.amountFactor, i); + // Use BigInt arithmetic to avoid scientific notation conversion + const originalValue = BigInt(recurringBid.amount.value.toString()); + const factorScaled = Math.floor(factor * 1000000); // Scale factor to avoid decimals + const adjustedValue = (originalValue * BigInt(factorScaled)) / BigInt(1000000); + adjustedAmount = { + ...recurringBid.amount, + value: adjustedValue.toString(), + }; + } + + if (recurringBid.priceFactor && i > 0) { + const factor = recurringBid.priceFactor * i; + // Use BigInt arithmetic to avoid scientific notation conversion + const originalValue = BigInt(recurringBid.price.value.toString()); + const factorScaled = Math.floor(factor * 1000000); // Scale factor to avoid decimals + const adjustedValue = (originalValue * BigInt(factorScaled)) / BigInt(1000000); + adjustedPrice = { + ...recurringBid.price, + value: adjustedValue.toString(), + }; + } + + const internalBid: InternalBidData = { + bidder: bidder.address, + type: BiddersType.NAMED, + bidData: { + atBlock: blockNumber, + amount: adjustedAmount, + price: adjustedPrice, + hookData: recurringBid.hookData, + expectRevert: undefined, + }, + }; + if (recurringBid.previousTick) { + internalBid.bidData.previousTick = + recurringBid.previousTick + (recurringBid.previousTickIncrement || 0) * i; + } + if (recurringBid.prevTickPrice) { + // Handle prevTickPriceIncrement as number or string (for huge values) + const basePrevTickPrice = BigInt(recurringBid.prevTickPrice); + const increment = recurringBid.prevTickPriceIncrement + ? typeof recurringBid.prevTickPriceIncrement === "string" + ? BigInt(recurringBid.prevTickPriceIncrement) + : BigInt(recurringBid.prevTickPriceIncrement) + : 0n; + internalBid.bidData.prevTickPrice = (basePrevTickPrice + increment * BigInt(i)).toString(); + } + bids.push(internalBid); + } + }); + }); + } + + // Group bidders + if (interactionData.groups) { + interactionData.groups.forEach((group) => { + const bidders = this.groupBidders.get(group.labelPrefix); + if (bidders) { + for (let round = 0; round < group.rounds; round++) { + for (let i = 0; i < bidders.length; i++) { + const bidder = bidders[i]; + const atBlock = + group.startBlock + round * group.betweenRoundStartsBlocks + i * group.rotationIntervalBlocks; + + bids.push({ + bidData: { + atBlock: atBlock, + amount: group.amount, + price: group.price, + hookData: group.hookData, + previousTick: group.previousTick, + prevTickPrice: group.prevTickPrice, + }, + bidder, + type: BiddersType.GROUP, + group: group.labelPrefix, + }); + } + } + } + }); + } + + return bids; + } + + /** + * Executes a single bid by creating and submitting the transaction. + * @param bid - Internal bid data containing bidder, bid data, and type + * @param transactionInfos - Array to collect transaction information + * @throws Error if bid execution fails or expected revert validation fails + */ + async executeBid(bid: InternalBidData, transactionInfos: TransactionInfo[]): Promise { + // This method just executes the bid transaction + + const bidData = bid.bidData; + const bidder = bid.bidder; + + // Calculate from tick number + const floorPrice = await this.auction.floorPrice(); + const tickSpacing = await this.auction.tickSpacing(); + // Use prevTickPrice if provided, otherwise calculate from previousTick + let previousTickPrice: bigint; + if (bidData.prevTickPrice) { + // Direct price hint provided + previousTickPrice = BigInt(bidData.prevTickPrice); + } else if (bidData.previousTick) { + previousTickPrice = tickNumberToPriceX96(bidData.previousTick, floorPrice, tickSpacing); + } else { + throw new Error(ERROR_MESSAGES.PREVIOUS_TICK_OR_TICK_PRICE_NOT_PROVIDED); + } + + const amount = await this.calculateAmount(bidData.amount); + const price = await calculatePrice(bidData.price, floorPrice, tickSpacing); + + if (this.currency) { + await this.grantPermit2Allowances(this.currency, bidder, transactionInfos); + } + let tx: ContractTransaction; // Transaction response type varies + let msg: string; + if (this.currency) { + msg = ` Bidding with ERC20 currency: ${await this.currency.getAddress()}`; + tx = await this.auction + .getFunction("submitBid(uint256,uint128,address,uint256,bytes)") + .populateTransaction(price, amount, bidder, previousTickPrice, bidData.hookData || "0x"); + } else { + // For native currency, send the required amount as msg.value + msg = ` Bidding with Native currency. Price: ${price.toString()}, Amount: ${amount.toString()}, Previous Tick Price: ${previousTickPrice.toString()}`; + tx = await this.auction + .getFunction("submitBid(uint256,uint128,address,uint256,bytes)") + .populateTransaction(price, amount, bidder, previousTickPrice, bidData.hookData || "0x", { + value: amount, + gasLimit: 2000000, + }); + } + transactionInfos.push({ tx, from: bidder, msg, expectRevert: bidData.expectRevert }); + } + + /** + * Validates that a transaction reverted with the expected error message. + * @param error - The error object from the failed transaction + * @param expectedRevert - The expected revert message to match + * @throws Error if the revert message doesn't match the expected value + */ + async validateExpectedRevert(error: unknown, expectedRevert: string): Promise { + // Extract the revert data string from the error + const errorObj = error as any; + let actualRevertData = errorObj?.data || errorObj?.error?.data || errorObj?.info?.data || ""; + + // Try to decode the revert reason from the data + if (actualRevertData && actualRevertData.startsWith("0x")) { + try { + // Remove the 0x prefix and decode the hex string + const hexData = actualRevertData.slice(2); + + // Check if it's a standard revert with reason (function selector 0x08c379a0) + if (hexData.startsWith("08c379a0")) { + // Skip the function selector (8 bytes) and decode the string + const reasonHex = hexData.slice(8); + const reasonBytes = Buffer.from(reasonHex, "hex"); + const decodedReason = reasonBytes.toString("utf8").replace(/\0/g, "").trim(); + + if (!actualRevertData.includes(expectedRevert) && decodedReason) { + actualRevertData = decodedReason; + } + } + } catch (decodeError) { + // If decoding fails, use the raw data + console.log(LOG_PREFIXES.INFO, "Could not decode revert reason, using raw data"); + } + } + + // Check if the revert data contains the expected string + if (!actualRevertData.includes(expectedRevert)) { + throw new Error(ERROR_MESSAGES.EXPECTED_REVERT_NOT_FOUND(expectedRevert, actualRevertData)); + } + console.log(LOG_PREFIXES.SUCCESS, "Expected revert validated:", expectedRevert); + } + + /** + * Parse variation string into an absolute amount. + * Supports both percentage format (e.g., "10%") and raw amount format. + * @param variation - Variation string in percentage or raw amount format + * @param baseValue - The base value to apply percentage to + * @returns Variation amount as a bigint + */ + private parseVariation(variation: string, baseValue: bigint): bigint { + if (variation.endsWith("%")) { + // Percentage: convert to ratio and apply to base value (e.g., "10%" of 1 ETH -> 0.1 ETH) + const percentage = parseFloat(variation.slice(0, -1)); + const ratio = percentage / 100; + const variationAmount = (baseValue * BigInt(Math.floor(ratio * 1000000))) / 1000000n; + return variationAmount; + } else { + // Raw amount: use as-is + return BigInt(variation.toString()); + } + } + + /** + * Calculates the actual bid amount based on the amount configuration. + * @param amountConfig - Amount configuration specifying type, side, value, and optional variance + * @returns The calculated amount as a bigint + * @throws Error if amount type is unsupported or PERCENT_OF_SUPPLY is used incorrectly + */ + async calculateAmount(amountConfig: AmountConfig): Promise { + // Ensure the value is treated as a string to avoid scientific notation conversion + let value: bigint = BigInt(amountConfig.value.toString()); + + if (amountConfig.variation) { + const variation = this.parseVariation(amountConfig.variation.toString(), value); + const randomVariation = Math.floor(Math.random() * (2 * Number(variation) + 1)) - Number(variation); + value = value + BigInt(randomVariation); + if (value < 0n) value = 0n; + } + + return value; + } + + /** + * Executes token transfer actions. + * @param transferInteractions - Array of transfer actions to execute + * @param transactionInfos - Array to collect transaction information + */ + async executeTransfers(transferInteractions: TransferAction[], transactionInfos: TransactionInfo[]): Promise { + // This handles token transfers between addresses + // Support for both ERC20 tokens and native currency + // Resolves label references for 'to' addresses + for (const interaction of transferInteractions) { + const { from, to, token, amount } = interaction.value; + const toAddress = this.labelMap.get(to) || to; + const amountValue = BigInt(amount.toString()); // Transfer actions use raw amounts directly + + // Execute the transfer based on token type + if (token === ZERO_ADDRESS) { + // Native ETH transfer + await this.executeNativeTransfer( + from, + toAddress, + amountValue, + interaction.value.expectRevert || "", + transactionInfos, + ); + } else { + // ERC20 token transfer - resolve token address first + const resolvedTokenAddress = await resolveTokenAddress(token, this.auctionDeployer); + await this.executeTokenTransfer( + from, + toAddress, + resolvedTokenAddress, + amountValue, + interaction.value.expectRevert || "", + transactionInfos, + ); + } + } + } + + /** + * Executes a native ETH transfer between addresses. + * @param from - The sender address + * @param to - The recipient address + * @param amount - The amount to transfer in wei + * @param expectRevert - Expected revert message if the transfer should fail + * @param transactionInfos - Array to collect transaction information + */ + private async executeNativeTransfer( + from: string, + to: string, + amount: bigint, + expectRevert: string, + transactionInfos: TransactionInfo[], + ): Promise { + // Send native currency + const tx = { + to, + value: amount, + }; + let msg = ` Native transfer: ${(parseFloat(amount.toString()) / 10 ** 18).toString()} ETH`; + transactionInfos.push({ tx, from, msg, expectRevert }); + } + + /** + * Executes a token transfer between addresses. + * @param from - The sender address + * @param to - The recipient address + * @param tokenAddress - The token contract address + * @param amount - The amount to transfer + * @param expectRevert - Expected revert message if the transfer should fail + * @param transactionInfos - Array to collect transaction information + */ + async executeTokenTransfer( + from: string, + to: string, + tokenAddress: string, + amount: bigint, + expectRevert: string, + transactionInfos: TransactionInfo[], + ): Promise { + // Get the token contract + const token = await hre.ethers.getContractAt("ERC20", tokenAddress); + let decimals = await token.decimals(); + let symbol = await token.symbol(); + // Execute the transfer + const tx = await token.getFunction("transfer").populateTransaction(to, amount); + let msg = ` Token transfer: ${(parseFloat(amount.toString()) / 10 ** Number(decimals)).toString()} ${symbol}`; + transactionInfos.push({ tx, from, msg, expectRevert }); + } + + /** + * Executes admin actions on the auction contract. + * @param adminInteractions - Array of admin action groups to execute + * @param transactionInfos - Array to collect transaction information + */ + async executeAdminActions(adminInteractions: AdminAction[], transactionInfos: TransactionInfo[]): Promise { + console.log(LOG_PREFIXES.INFO, "Executing admin actions:", adminInteractions); + for (const interaction of adminInteractions) { + if (interaction.method === AdminActionMethod.CHECKPOINT) { + let tx = await this.auction.getFunction("checkpoint").populateTransaction(); + let msg = ` Creating checkpoint`; + transactionInfos.push({ tx, from: null, msg }); + } else if (interaction.method === AdminActionMethod.SWEEP_CURRENCY) { + let tx = await this.auction.getFunction("sweepCurrency").populateTransaction(); + let msg = ` Sweeping currency`; + transactionInfos.push({ tx, from: null, msg }); + } else if (interaction.method === AdminActionMethod.SWEEP_UNSOLD_TOKENS) { + let tx = await this.auction.getFunction("sweepUnsoldTokens").populateTransaction(); + let msg = ` Sweeping unsold tokens`; + transactionInfos.push({ tx, from: null, msg }); + } + } + } + + /** + * Grants Permit2 allowances for a bidder to interact with the auction. + * @param currency - The currency contract to grant allowance for + * @param bidder - The bidder address to grant allowance to + * @param transactionInfos - Array to collect transaction information + */ + async grantPermit2Allowances(currency: Contract, bidder: string, transactionInfos: TransactionInfo[]): Promise { + // First, approve Permit2 to spend the tokens + const approveTx = await currency.getFunction("approve").populateTransaction(PERMIT2_ADDRESS, MAX_UINT256); + let approveMsg = ` Approving Permit2 to spend tokens`; + transactionInfos.push({ tx: approveTx, from: bidder, msg: approveMsg }); + + // Then, call Permit2's approve function to grant allowance to the auction contract + const permit2 = (await hre.ethers.getContractAt( + "IAllowanceTransfer", + PERMIT2_ADDRESS, + )) as unknown as IAllowanceTransfer; + const auctionAddress = await this.auction.getAddress(); + const maxAmount = UINT_160_MAX; // uint160 max + const maxExpiration = UINT_48_MAX; // uint48 max (far in the future) + let tx = await permit2.getFunction("approve").populateTransaction( + await currency.getAddress(), // token address + auctionAddress, // spender (auction contract) + maxAmount, // amount (max uint160) + maxExpiration, // expiration (max uint48) + ); + let msg = ` Granting Permit2 allowance to the auction contract`; + transactionInfos.push({ tx, from: bidder, msg }); + } + public getReverseLabelMap(): Map { + return this.reverseLabelMap; + } +} diff --git a/test/e2e/src/E2ECliRunner.ts b/test/e2e/src/E2ECliRunner.ts new file mode 100644 index 00000000..763a50ad --- /dev/null +++ b/test/e2e/src/E2ECliRunner.ts @@ -0,0 +1,218 @@ +#!/usr/bin/env node + +import { MultiTestRunner, CombinationResult } from "./MultiTestRunner"; +import { TestInstance } from "./SchemaValidator"; +import { TestSetupData } from "../schemas/TestSetupSchema"; +import { TestInteractionData } from "../schemas/TestInteractionSchema"; +import { LOG_PREFIXES, ERROR_MESSAGES, SETUP, INTERACTION } from "./constants"; +import * as fs from "fs"; +import * as path from "path"; + +interface CombinationToRun { + setup: TestSetupData; + interaction: TestInteractionData; +} + +interface AvailableFiles { + setup: string[]; + interaction: string[]; +} + +class E2ECliRunner { + private runner: MultiTestRunner; + + constructor() { + this.runner = new MultiTestRunner(); + } + + /** + * Converts a test name to PascalCase filename. + * Examples: "simple" โ†’ "SimpleSetup", "erc20" โ†’ "ERC20Setup", "extended" โ†’ "ExtendedSetup" + */ + private toFileName(testName: string, type: "setup" | "interaction"): string { + const suffix = type === "setup" ? "Setup" : "Interaction"; + // Capitalize first letter + const pascalName = testName.charAt(0).toUpperCase() + testName.slice(1); + return pascalName + suffix; + } + + /** + * Gets available test names by scanning the instances directory. + * @returns Array of test names (without Setup/Interaction suffix) + */ + getAvailableTestNames(): string[] { + const setupDir = path.join(__dirname, "../instances/setup"); + if (!fs.existsSync(setupDir)) return []; + + const files = fs.readdirSync(setupDir); + return files.filter((file) => file.endsWith(".ts")).map((file) => file.replace(/Setup\.ts$/, "").toLowerCase()); + } + + /** + * Runs all specified test combinations. + * @param combinations - Array of combinations to run + * @returns Array of combination results + */ + async runAllCombinations(combinations: CombinationToRun[]): Promise { + const results: CombinationResult[] = []; + + for (const combination of combinations) { + try { + console.log(LOG_PREFIXES.INFO, "Running:", combination.setup.name, "+", combination.interaction.name); + const result = await this.runner.runCombination(combination.setup, combination.interaction); + results.push(result); + console.log(LOG_PREFIXES.SUCCESS, "Success:", combination.setup.name, "+", combination.interaction.name); + } catch (error: unknown) { + console.error(LOG_PREFIXES.ERROR, "Failed:", combination.setup.name, "+", combination.interaction.name); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(LOG_PREFIXES.ERROR, "Error:", errorMessage); + results.push({ + setupName: combination.setup.name, + interactionName: combination.interaction.name, + success: false, + error: errorMessage, + }); + } + } + + return results; + } +} + +/** + * Loads test combinations from CLI arguments or returns all available tests. + * Test names can be provided without the "Setup" suffix (e.g., "simple", "extended"). + * @param runner - The E2ECliRunner instance + * @returns Array of combinations to run + */ +async function loadCombinationsFromArgs(runner: E2ECliRunner): Promise { + const args = process.argv.slice(2).filter((arg) => !arg.startsWith("-")); // Filter out flags + const availableTests = runner.getAvailableTestNames(); + + if (args.length === 0) { + // No arguments provided, run all available tests + console.log("๐Ÿ“‹ No arguments provided, running all", availableTests.length, "available tests"); + const combinations: CombinationToRun[] = []; + + for (const testName of availableTests) { + const setupFile = runner["toFileName"](testName, "setup"); + const interactionFile = runner["toFileName"](testName, "interaction"); + + try { + const setup = loadInstanceFromFile(setupFile, SETUP); + const interaction = loadInstanceFromFile(interactionFile, INTERACTION); + combinations.push({ setup, interaction }); + } catch (error) { + console.warn(LOG_PREFIXES.WARNING, `Skipping ${testName}: ${error}`); + } + } + + return combinations; + } + + const combinations: CombinationToRun[] = []; + + for (const testName of args) { + const normalizedName = testName.toLowerCase(); + + // Convert test name to file names + const setupFile = runner["toFileName"](normalizedName, "setup"); + const interactionFile = runner["toFileName"](normalizedName, "interaction"); + + try { + const setup = loadInstanceFromFile(setupFile, SETUP); + const interaction = loadInstanceFromFile(interactionFile, INTERACTION); + combinations.push({ setup, interaction }); + console.log(LOG_PREFIXES.SUCCESS, "Loaded test:", testName, "โ†’", setup.name, "+", interaction.name); + } catch (error) { + console.error(LOG_PREFIXES.ERROR, "Failed to load test:", testName); + console.error(LOG_PREFIXES.ERROR, "Error:", error); + console.log("\n๐Ÿ“ Available tests:", availableTests.join(", ")); + process.exit(1); + } + } + + return combinations; +} + +/** + * Loads a TypeScript instance from a file using the SchemaValidator. + * @param fileName - The file name to load (e.g., "SimpleSetup", "ExtendedInteraction") + * @param type - The type of instance to load ("setup" or "interaction") + * @returns The loaded test instance data + * @throws Error if the file cannot be loaded or no instance is found + */ +function loadInstanceFromFile(fileName: string, type: "setup"): TestSetupData; +function loadInstanceFromFile(fileName: string, type: "interaction"): TestInteractionData; +function loadInstanceFromFile(fileName: string, type: "setup" | "interaction"): TestSetupData | TestInteractionData { + const { SchemaValidator } = require("./SchemaValidator"); + const validator = new SchemaValidator(); + return validator.loadTestInstance(type, fileName); +} + +/** + * Main entry point for the E2E test runner. + * Loads test combinations from CLI arguments or uses predefined combinations, + * runs all tests, and displays results with success/failure statistics. + */ +async function main(): Promise { + const runner = new E2ECliRunner(); + + console.log(LOG_PREFIXES.RUN, "TWAP Auction E2E Test Runner"); + console.log("================================"); + + // Show available tests + const availableTests = runner.getAvailableTestNames(); + console.log("\n๐Ÿ“ Available tests:", availableTests.join(", ")); + + // Load combinations from CLI arguments or use all available + const combinationsToRun = await loadCombinationsFromArgs(runner); + + // Run the specified combinations + console.log(LOG_PREFIXES.INFO, "Running", combinationsToRun.length, "specified combinations..."); + const results = await runner.runAllCombinations(combinationsToRun); + + // Summary + const passed = results.filter((r) => r.success).length; + const failed = results.filter((r) => !r.success).length; + + console.log(LOG_PREFIXES.INFO, "Final Summary:"); + console.log(LOG_PREFIXES.INFO, "Total combinations:", results.length); + console.log(LOG_PREFIXES.SUCCESS, "Passed:", passed); + console.log(LOG_PREFIXES.ERROR, "Failed:", failed); + + if (failed > 0) { + console.log(LOG_PREFIXES.ERROR, "Failed combinations:"); + results + .filter((r) => !r.success) + .forEach((r) => { + console.log(LOG_PREFIXES.ERROR, "-", r.setupName, "+", r.interactionName + ":", r.error); + }); + process.exit(1); + } else { + console.log(LOG_PREFIXES.SUCCESS, "All tests passed!"); + process.exit(0); + } +} + +// Show usage information +if (process.argv.includes("--help") || process.argv.includes("-h")) { + console.log("\n๐Ÿ“– E2E CLI Test Runner"); + console.log("======================\n"); + console.log("Usage:"); + console.log(" npm run e2e:run [test1] [test2] ..."); + console.log(" npm run e2e:run (runs all tests)\n"); + console.log("Examples:"); + console.log(" npm run e2e:run extended (run just Extended test)"); + console.log(" npm run e2e:run simple erc20 (run Simple and ERC20 tests)"); + console.log(" npm run e2e:run (run all available tests)"); + console.log(" npm run e2e:run extended > log.txt (save output to file)\n"); + console.log("Test names are auto-discovered from test/e2e/instances/"); + console.log("Each test loads matching Setup + Interaction files."); + process.exit(0); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/test/e2e/src/MultiTestRunner.ts b/test/e2e/src/MultiTestRunner.ts new file mode 100644 index 00000000..bb403bcd --- /dev/null +++ b/test/e2e/src/MultiTestRunner.ts @@ -0,0 +1,99 @@ +import { SingleTestRunner, TestResult } from "./SingleTestRunner"; +import { TestInstance } from "./SchemaValidator"; +import { TestSetupData } from "../schemas/TestSetupSchema"; +import { TestInteractionData } from "../schemas/TestInteractionSchema"; +import { INTERACTION, LOG_PREFIXES, SETUP } from "./constants"; + +export interface CombinationResult { + success: boolean; + result?: TestResult; + error?: string; + setupName: string; + interactionName: string; +} + +export interface AvailableFiles { + setup: string[]; + interaction: string[]; +} + +export interface Combination { + setup: TestSetupData; + interaction: TestInteractionData; +} + +export class MultiTestRunner { + private singleTestRunner: SingleTestRunner; + + constructor() { + this.singleTestRunner = new SingleTestRunner(); + } + + /** + * Runs a specific setup and interaction combination. + * @param setupData - Test setup data + * @param interactionData - Test interaction data + * @returns Combination result with success status and any errors + */ + async runCombination(setupData: TestSetupData, interactionData: TestInteractionData): Promise { + const setupName = setupData.name; + const interactionName = interactionData.name; + console.log(LOG_PREFIXES.INFO, "Running combination:", setupName, "+", interactionName); + + try { + const result = await this.singleTestRunner.runFullTest(setupData, interactionData); + console.log(LOG_PREFIXES.SUCCESS, setupName, "+", interactionName, "- PASSED"); + return { success: true, result, setupName, interactionName }; + } catch (error: unknown) { + console.error(LOG_PREFIXES.ERROR, setupName, "+", interactionName, "- FAILED"); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(LOG_PREFIXES.ERROR, "Error:", errorMessage); + return { success: false, error: errorMessage, setupName, interactionName }; + } + } + + /** + * Runs all test combinations and returns results. + * @param combinations - Array of setup and interaction combinations to test + * @returns Array of combination results + */ + async runAllCombinations(combinations: Combination[]): Promise { + console.log(LOG_PREFIXES.INFO, "Running", combinations.length, "test combinations..."); + + const results: CombinationResult[] = []; + let passed = 0; + let failed = 0; + + for (const combo of combinations) { + const result = await this.runCombination(combo.setup, combo.interaction); + results.push(result); + + if (result.success) { + passed++; + } else { + failed++; + } + } + + console.log(LOG_PREFIXES.INFO, "Test Results:"); + console.log(LOG_PREFIXES.SUCCESS, "Passed:", passed); + console.log(LOG_PREFIXES.ERROR, "Failed:", failed); + console.log(LOG_PREFIXES.INFO, "Success Rate:", ((passed / combinations.length) * 100).toFixed(1) + "%"); + + return results; + } + + /** + * Gets available setup and interaction files for testing. + * @returns Object containing arrays of available setup and interaction files + */ + getAvailableFiles(): AvailableFiles { + const setupFiles = this.singleTestRunner["schemaValidator"].getAllTestInstances(SETUP); + const interactionFiles = this.singleTestRunner["schemaValidator"].getAllTestInstances(INTERACTION); + + return { + setup: setupFiles.map((f: TestInstance) => f.filename), + interaction: interactionFiles.map((f: TestInstance) => f.filename), + }; + } +} diff --git a/test/e2e/src/SchemaValidator.ts b/test/e2e/src/SchemaValidator.ts new file mode 100644 index 00000000..4bdcdb79 --- /dev/null +++ b/test/e2e/src/SchemaValidator.ts @@ -0,0 +1,128 @@ +import * as fs from "fs"; +import * as path from "path"; +import { TestSetupData } from "../schemas/TestSetupSchema"; +import { TestInteractionData } from "../schemas/TestInteractionSchema"; +import { register } from "ts-node"; +import { ERROR_MESSAGES, SETUP } from "./constants"; + +export interface TestInstance { + filename: string; + data: TestSetupData | TestInteractionData; +} + +export class SchemaValidator { + /** + * Loads a test instance from a TypeScript file. + * @param type - Type of test instance ("setup" or "interaction") + * @param filename - Name of the file to load + * @returns The loaded test instance data + * @throws Error if file is not found or no export is found + */ + loadTestInstance(type: "setup" | "interaction", filename: string): TestSetupData | TestInteractionData { + // Only load TypeScript files + if (filename.endsWith(".ts")) { + const baseName = filename.replace(".ts", ""); + return this.loadTypeScriptInstance(type, baseName); + } else { + // Try to find .ts version + const tsFilePath = path.join(__dirname, `../instances/${type}/${filename}.ts`); + + if (fs.existsSync(tsFilePath)) { + return this.loadTypeScriptInstance(type, filename); + } else { + throw new Error(ERROR_MESSAGES.TYPESCRIPT_FILE_NOT_FOUND(tsFilePath)); + } + } + } + + /** + * Converts a filename or test name to the expected export name (camelCase). + * @param filename - The filename or test name to convert + * @param type - The type of instance ("setup" or "interaction") + * @returns The expected export name in camelCase format + * @example + * toExportName("SimpleSetup", "setup") โ†’ "simpleSetup" + * toExportName("ERC20Setup", "setup") โ†’ "erc20Setup" + * toExportName("simple", "setup") โ†’ "simpleSetup" + * toExportName("extended", "setup") โ†’ "extendedSetup" + */ + private toExportName(filename: string, type: "setup" | "interaction"): string { + // Remove .ts extension if present + let name = filename.replace(/\.ts$/, ""); + + // If it doesn't end with Setup/Interaction, add it + if (type === "setup" && !name.endsWith("Setup")) { + // Capitalize first letter: "simple" โ†’ "Simple", "erc20" โ†’ "Erc20" + name = name.charAt(0).toUpperCase() + name.slice(1) + "Setup"; + } else if (type === "interaction" && !name.endsWith("Interaction")) { + name = name.charAt(0).toUpperCase() + name.slice(1) + "Interaction"; + } + + // Convert to camelCase: "SimpleSetup" โ†’ "simpleSetup", "ERC20Setup" โ†’ "erc20Setup" + return name.charAt(0).toLowerCase() + name.slice(1); + } + + /** + * Loads a TypeScript instance from a file using ts-node. + * @param type - Type of test instance ("setup" or "interaction") + * @param filename - Name of the file to load + * @returns The loaded test instance data + * @throws Error if file is not found or no export is found + */ + private loadTypeScriptInstance(type: "setup" | "interaction", filename: string): TestSetupData | TestInteractionData { + let modulePath: string; + try { + if (type === SETUP) { + // Use ts-node to load TypeScript files directly + modulePath = path.join(__dirname, `../instances/setup/${filename}.ts`); + } else { + // Use ts-node to load TypeScript files directly + modulePath = path.join(__dirname, `../instances/interaction/${filename}.ts`); + } + delete require.cache[modulePath]; + + // Register ts-node + register(); + + // TODO: find a way to avoid require + const module = require(modulePath); + + // Get the expected export name using the new conversion + const exportName = this.toExportName(filename, type); + const data = module[exportName] || module.default; + + if (!data) { + throw new Error(ERROR_MESSAGES.NO_EXPORT_FOUND(filename, Object.keys(module).join(", "))); + } + + return data; + } catch (error) { + throw new Error( + ERROR_MESSAGES.FAILED_TO_LOAD_TYPESCRIPT_INSTANCE( + filename, + error instanceof Error ? error.message : String(error), + ), + ); + } + } + + /** + * Gets all available test instances of a specific type. + * @param type - Type of test instances to retrieve ("setup" or "interaction") + * @returns Array of test instances with filename and data + */ + getAllTestInstances(type: "setup" | "interaction"): TestInstance[] { + const instancesDir = path.join(__dirname, `../instances/${type}`); + if (!fs.existsSync(instancesDir)) return []; + + const files = fs.readdirSync(instancesDir); + + // Only look for TypeScript files + return files + .filter((file) => file.endsWith(".ts")) + .map((file) => ({ + filename: file, + data: this.loadTestInstance(type, file), + })); + } +} diff --git a/test/e2e/src/SingleTestRunner.ts b/test/e2e/src/SingleTestRunner.ts new file mode 100644 index 00000000..2ede84b7 --- /dev/null +++ b/test/e2e/src/SingleTestRunner.ts @@ -0,0 +1,1082 @@ +import { SchemaValidator } from "./SchemaValidator"; +import { Address, TestSetupData } from "../schemas/TestSetupSchema"; +import { + ActionType, + TestInteractionData, + AdminAction, + TransferAction, + AssertionInfo, +} from "../schemas/TestInteractionSchema"; +import { AuctionDeployer } from "./AuctionDeployer"; +import { BidSimulator, InternalBidData } from "./BidSimulator"; +import { AssertionEngine, AuctionState } from "./AssertionEngine"; +import { Contract, Interface } from "ethers"; +import { Network } from "hardhat/types"; +import { + PERMIT2_ADDRESS, + LOG_PREFIXES, + ERROR_MESSAGES, + METHODS, + TYPES, + PENDING_STATE, + EVENTS, + ZERO_ADDRESS, + NATIVE_CURRENCY_NAME, +} from "./constants"; +import { artifacts } from "hardhat"; +import { EventData, HashWithRevert, TransactionInfo, EventType, ActionData, TokenContract } from "./types"; +import { TransactionRequest } from "ethers"; +import { parseBoolean } from "./utils"; +import { GroupConfig } from "../schemas/TestSetupSchema"; +import hre from "hardhat"; + +export interface TestResult { + setupData: TestSetupData; + interactionData: TestInteractionData; + auction: Contract; + auctionedToken: Address | null; + currencyToken: Address | null; + finalState: AuctionState; + success: boolean; +} + +interface BidInfo { + bidId: number; + owner: string; + maxPrice: bigint; + amount: bigint; +} + +interface CheckpointInfo { + blockNumber: number; + clearingPrice: bigint; +} + +export class SingleTestRunner { + private network: Network; + private schemaValidator: SchemaValidator; + private deployer: AuctionDeployer; + private cachedInterface: Interface | null = null; + private bidsByOwner: Map = new Map(); // Track bid info per owner + private startBalancesMap: Map> = new Map(); // Track start balances for each owner + private checkpoints: CheckpointInfo[] = []; // Track all checkpoints + private auction: Contract | null = null; // Store auction for bid tracking + + constructor() { + this.network = hre.network; + this.schemaValidator = new SchemaValidator(); + this.deployer = new AuctionDeployer(); + } + + /** + * Runs a complete E2E test from setup to completion. + * @param setupData - Test setup data containing auction parameters and environment + * @param interactionData - Test interaction data containing bids, actions, and assertions + * @returns Test result containing success status and any errors + * @throws Error if test setup fails or current block is past auction start block + */ + async runFullTest(setupData: TestSetupData, interactionData: TestInteractionData): Promise { + console.log(LOG_PREFIXES.TEST, "Running test:", setupData.name, "+", interactionData.name); + + console.log(LOG_PREFIXES.SUCCESS, "Schema validation passed"); + + // Clear instance state from previous tests + this.bidsByOwner.clear(); + this.checkpoints = []; + this.auction = null; + this.cachedInterface = null; + + // Reset the Hardhat network to start fresh for each test + // Deploy Permit2 at canonical address (one-time setup) + await this.resetChainWithPermit2(); + console.log(LOG_PREFIXES.INFO, "Reset Hardhat network to start fresh"); + + // PHASE 1: Setup the auction environment + console.log(LOG_PREFIXES.INFO, "Phase 1: Setting up auction environment..."); + + // Check current block and auction start block + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const auctionStartBlock = parseInt(setupData.env.startBlock) + (setupData.env.offsetBlocks ?? 0); + console.log(LOG_PREFIXES.CONFIG, "Current block:", currentBlock, ", Auction start block:", auctionStartBlock); + + if (currentBlock < auctionStartBlock) { + const blocksToMine = auctionStartBlock - currentBlock; + await hre.ethers.provider.send(METHODS.HARDHAT.MINE, [`0x${blocksToMine.toString(16)}`]); + console.log(LOG_PREFIXES.INFO, "Mined", blocksToMine, "blocks to reach auction start block", auctionStartBlock); + } else if (currentBlock > auctionStartBlock) { + throw new Error(ERROR_MESSAGES.BLOCK_ALREADY_PAST_START(currentBlock, auctionStartBlock)); + } + + let setupTransactions: TransactionInfo[] = []; + // Initialize deployer with tokens and factory (one-time setup) + await this.deployer.initialize(setupData, setupTransactions); + this.generateAddressesForGroups(setupData.env.groups ?? []); + console.log(LOG_PREFIXES.INFO, "Generated addresses for groups: ", setupData.env.groups); + // Setup balances + await this.deployer.setupBalances(setupData, setupTransactions, this.startBalancesMap); + await this.handleInitializeTransactions(setupTransactions); + setupTransactions = []; + // Create the auction + const auction: Contract = await this.deployer.createAuction(setupData, setupTransactions); + this.auction = auction; // Store for bid tracking + + console.log(LOG_PREFIXES.AUCTION, "Auction deployed:", await auction.getAddress()); + + // PHASE 2: Execute interactions on the configured auction + console.log(LOG_PREFIXES.PHASE, "Phase 2: Executing interaction scenario..."); + const auctionedToken = this.deployer.getTokenByName(setupData.auctionParameters.auctionedToken) ?? null; + const currencyToken = + setupData.auctionParameters.currency === "0x0000000000000000000000000000000000000000" + ? null + : this.deployer.getTokenByName(setupData.auctionParameters.currency) ?? null; + const bidSimulator = new BidSimulator(auction, currencyToken as Contract, this.deployer); + const assertionEngine = new AssertionEngine(auction, auctionedToken, currencyToken, this.deployer); + + // Setup labels and execute the interaction scenario + await bidSimulator.setupLabels(interactionData, setupData); + + // Execute bids and actions with integrated checkpoint validation + await this.executeWithAssertions( + bidSimulator, + assertionEngine, + interactionData, + setupTransactions, + setupData.env.startBlock, + setupData.env.offsetBlocks ?? 0, + ); + + console.log(LOG_PREFIXES.SUCCESS, "Bids executed and assertions validated successfully"); + + // Get final state at the end of the auction + const blockBeforeFinalState = await hre.ethers.provider.getBlockNumber(); + const auctionEndBlock = + parseInt(setupData.env.startBlock) + + setupData.auctionParameters.auctionDurationBlocks + + (setupData.env.offsetBlocks ?? 0); + + // Mine to after the auction ends if needed to get accurate final state + if (blockBeforeFinalState < auctionEndBlock) { + const blocksToMine = auctionEndBlock - blockBeforeFinalState + 1; + await hre.ethers.provider.send("hardhat_mine", [`0x${blocksToMine.toString(16)}`]); + } + + // Get final state as-is (without forcing a checkpoint) + const finalState = await assertionEngine.getAuctionState(); + + // Don't add final state as a checkpoint - only use checkpoints from actual events + // The contract only validates hints against stored checkpoints from CheckpointUpdated events + let calculatedCurrencyRaised = -1; + // Exit and claim all bids if auction graduated + if (finalState.isGraduated) { + calculatedCurrencyRaised = await this.exitAndClaimAllBids( + auction, + setupData, + currencyToken, + bidSimulator.getReverseLabelMap(), + ); + } + + // Log balances for all funded accounts (after claiming) + await this.logAccountBalances(setupData, this.deployer, auctionedToken); + this.logFinalState(finalState); + console.log(LOG_PREFIXES.FINAL, "Test completed successfully!"); + await this.logSummary(auction, currencyToken, auctionedToken, finalState, calculatedCurrencyRaised); + return { + setupData, + interactionData, + auction, + auctionedToken: auctionedToken ? ((await auctionedToken.getAddress()) as Address) : null, + currencyToken: currencyToken ? ((await currencyToken.getAddress()) as Address) : null, + finalState, + success: true, + }; + } + + private async logSummary( + auction: Contract, + currencyToken: TokenContract | null, + auctionedToken: TokenContract | null, + finalState: AuctionState, + totalCurrencyRaised: number, + ): Promise { + let bidCount = 0; + for (const [, bids] of this.bidsByOwner.entries()) { + bidCount += bids.length; + } + const auctionDurationBlocks = (await auction.endBlock()) - (await auction.startBlock()); + + let auctionCurrencyBalance = 0; + if (currencyToken === null) { + auctionCurrencyBalance = Number(await hre.ethers.provider.getBalance(auction.getAddress())); + auctionCurrencyBalance /= 10 ** 18; + } else { + auctionCurrencyBalance = Number(await currencyToken.balanceOf(auction.getAddress())); + const decimals = await currencyToken.decimals(); + auctionCurrencyBalance /= 10 ** Number(decimals); + } + + let expectedCurrencyRaised = Number(finalState.currencyRaised); + if (auctionedToken !== null) { + const decimals = await auctionedToken.decimals(); + expectedCurrencyRaised /= 10 ** Number(decimals); + } else { + throw new Error(ERROR_MESSAGES.AUCTIONED_TOKEN_IS_NULL); + } + + console.log("\n=== After exit and token claims ==="); + console.log(LOG_PREFIXES.INFO, "Bid count:", bidCount); + console.log(LOG_PREFIXES.INFO, "Auction duration (blocks):", auctionDurationBlocks); + console.log(LOG_PREFIXES.INFO, "Auction currency balance:", auctionCurrencyBalance); + + if (totalCurrencyRaised !== -1) { + console.log(LOG_PREFIXES.INFO, "Actual currency raised (from all bids after refunds):", totalCurrencyRaised); + } + + console.log(LOG_PREFIXES.INFO, "Expected currency raised (for sweepCurrency()):", expectedCurrencyRaised); + console.log("\n============================\n"); + } + + private logFinalState(finalState: AuctionState): void { + console.log(LOG_PREFIXES.CONFIG, "Final state (as of block", finalState.currentBlock + "):"); + console.log(" Is graduated:", finalState.isGraduated); + console.log(" Clearing price:", finalState.clearingPrice); + console.log(" Currency raised:", finalState.currencyRaised); + console.log(" Latest checkpoint:"); + console.log(" Clearing price:", finalState.latestCheckpoint.clearingPrice); + console.log(" Currency raised (Q96_X7):", finalState.latestCheckpoint.currencyRaisedQ96_X7); + console.log( + " Currency raised at clearing price (Q96_X7):", + finalState.latestCheckpoint.currencyRaisedAtClearingPriceQ96_X7, + ); + console.log(" Cumulative MPS per price:", finalState.latestCheckpoint.cumulativeMpsPerPrice); + console.log(" Cumulative MPS:", finalState.latestCheckpoint.cumulativeMps); + console.log(" Prev:", finalState.latestCheckpoint.prev); + console.log(" Next:", finalState.latestCheckpoint.next); + } + + private generateAddressesForGroups(groups: GroupConfig[]): void { + for (const group of groups) { + if (!group.addresses) { + group.addresses = []; + } + for (let i = 0; i < group.count; i++) { + group.addresses.push(hre.ethers.Wallet.createRandom().address as Address); + } + } + } + + /** + * Logs balances for all funded accounts showing initial vs final balances + */ + private async logAccountBalances( + setupData: TestSetupData, + deployer: AuctionDeployer, + auctionedToken: TokenContract | null, + ): Promise { + if (!setupData.env.balances || setupData.env.balances.length === 0) return; + + console.log("\n๐Ÿ“Š Account Balances Summary:"); + console.log("============================"); + + // Collect unique addresses + const addresses = [...new Set(setupData.env.balances.map((b) => b.address))]; + + for (const address of addresses) { + console.log(`\n${address}:`); + + // Get all initial balances for this address + const addressBalances = setupData.env.balances.filter((b) => b.address === address); + + // Check each token they were funded with + for (const initialBalance of addressBalances) { + const tokenIdentifier = initialBalance.token; + const initialAmount = BigInt(initialBalance.amount); + + let currentBalance: bigint; + let tokenSymbol: string; + let decimals = 18; + + if (tokenIdentifier === ZERO_ADDRESS) { + // Native ETH + currentBalance = await hre.ethers.provider.getBalance(address); + tokenSymbol = NATIVE_CURRENCY_NAME; + } else { + // ERC20 token + const tokenContract = deployer.getTokenByName(tokenIdentifier); + if (tokenContract) { + currentBalance = await tokenContract.balanceOf(address); + tokenSymbol = await tokenContract.symbol(); + decimals = Number(await tokenContract.decimals()); + } else { + console.log(` โš ๏ธ Token ${tokenIdentifier} not found`); + continue; + } + } + + const difference = currentBalance - initialAmount; + const diffSymbol = difference >= 0n ? "+" : ""; + const diffValue = Number(difference) / 10 ** decimals; + + console.log(` ${tokenSymbol}:`); + console.log(` Initial: ${(Number(initialAmount) / 10 ** decimals).toFixed(6)}`); + console.log(` Final: ${(Number(currentBalance) / 10 ** decimals).toFixed(6)}`); + console.log(` Diff: ${diffSymbol}${diffValue.toFixed(6)}`); + } + + // Also check auctioned token balance (tokens received from auction) + if (auctionedToken) { + const tokenBalance = await auctionedToken.balanceOf(address); + if (tokenBalance > 0n) { + const tokenSymbol = await auctionedToken.symbol(); + const decimals = await auctionedToken.decimals(); + console.log(` ${tokenSymbol} (claimed from auction):`); + console.log(` Balance: ${(Number(tokenBalance) / 10 ** Number(decimals)).toFixed(6)}`); + } + } + } + + const auctionCurrency = setupData.auctionParameters.currency; + const auctionCurrencyIsNative = + auctionCurrency == ZERO_ADDRESS || (auctionCurrency as string).toLowerCase().includes("native"); + const groupBalances = setupData.env.groups; + if (groupBalances) { + for (const group of groupBalances) { + if (!group.addresses) continue; + console.log(`\n\n${group.labelPrefix}:`); + for (let i = 0; i < group.addresses.length; i++) { + console.log(`\n Group Member ID: ${group.labelPrefix}-${i}:`); + const groupMember = group.addresses[i]; + const currentNativeBalance = await hre.ethers.provider.getBalance(groupMember); + let initialNativeBalance = BigInt(group.startNativeEach ?? "0"); + if (auctionCurrencyIsNative) { + initialNativeBalance += BigInt(group.startAmountEach ?? "0"); + } + + const difference = currentNativeBalance - initialNativeBalance; + const diffSymbol = difference >= 0n ? "+" : ""; + const diffValue = Number(difference) / 10 ** 18; + + console.log(` ${NATIVE_CURRENCY_NAME}:`); + console.log(` Initial: ${(Number(initialNativeBalance) / 10 ** 18).toFixed(6)}`); + console.log(` Final: ${(Number(currentNativeBalance) / 10 ** 18).toFixed(6)}`); + console.log(` Diff: ${diffSymbol}${diffValue.toFixed(6)}`); + if (group.startAmountEach && !auctionCurrencyIsNative) { + let auctionCurrencyContract: TokenContract | undefined; + if (auctionCurrency.startsWith("0x")) { + auctionCurrencyContract = deployer.getTokenByName(auctionCurrency as Address); + } else { + auctionCurrencyContract = deployer.getTokenByName(auctionCurrency); + } + const currentAuctionCurrencyBalance = await auctionCurrencyContract?.balanceOf(groupMember); + const initialAuctionCurrencyBalance = BigInt(group.startAmountEach); + + const difference = currentAuctionCurrencyBalance - initialAuctionCurrencyBalance; + const diffSymbol = difference >= 0n ? "+" : ""; + + const decimals = await auctionCurrencyContract?.decimals(); + const diffValue = Number(difference) / 10 ** Number(decimals); + + const tokenSymbol = await auctionCurrencyContract?.symbol(); + + console.log(` ${tokenSymbol}:`); + console.log( + ` Initial: ${(Number(initialAuctionCurrencyBalance) / 10 ** Number(decimals)).toFixed(6)}`, + ); + console.log( + ` Final: ${(Number(currentAuctionCurrencyBalance) / 10 ** Number(decimals)).toFixed(6)}`, + ); + console.log(` Diff: ${diffSymbol}${diffValue.toFixed(6)}`); + } + if (auctionedToken) { + const currentAuctionedTokenBalance = await auctionedToken.balanceOf(groupMember); + if (currentAuctionedTokenBalance == 0n) continue; + const decimals = await auctionedToken.decimals(); + + const tokenSymbol = await auctionedToken.symbol(); + + console.log(` ${tokenSymbol} (claimed from auction):`); + console.log( + ` Balance: ${(Number(currentAuctionedTokenBalance) / 10 ** Number(decimals)).toFixed(6)}`, + ); + } + } + } + } + + console.log("\n============================\n"); + } + + /** + * Exit and claim all bids for the auction + */ + private async exitAndClaimAllBids( + auction: Contract, + setupData: TestSetupData, + currencyToken: TokenContract | null, + reverseLabelMap: Map, + ): Promise { + let calculatedCurrencyRaised = 0; + console.log("\n๐ŸŽซ Exiting and claiming all bids..."); + + // Mine to claim block if needed + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const claimBlock = + parseInt(setupData.env.startBlock) + + setupData.auctionParameters.auctionDurationBlocks + + setupData.auctionParameters.claimDelayBlocks + + (setupData.env.offsetBlocks ?? 0); + + if (currentBlock < claimBlock) { + const blocksToMine = claimBlock - currentBlock; + await hre.ethers.provider.send("hardhat_mine", [`0x${blocksToMine.toString(16)}`]); + console.log(` Mined ${blocksToMine} blocks to reach claim block ${claimBlock}`); + } + + // Sort checkpoints by block number for searching + const sortedCheckpoints = [...this.checkpoints].sort((a, b) => a.blockNumber - b.blockNumber); + const finalCheckpoint = sortedCheckpoints[sortedCheckpoints.length - 1]; + + console.log( + ` ๐Ÿ“Š ${sortedCheckpoints.length} checkpoints tracked, final clearing: ${ + finalCheckpoint?.clearingPrice || "N/A" + }`, + ); + console.log("Sorted checkpoints: ", sortedCheckpoints); + // Exit and claim for each owner + for (const [owner, bids] of this.bidsByOwner.entries()) { + if (bids.length === 0) continue; + const ownerLabel = reverseLabelMap.get(owner); + if (ownerLabel) { + console.log(`\n ${ownerLabel}: ${bids.length} bid(s)`); + } else { + console.log(`\n ${owner}: ${bids.length} bid(s)`); + } + + const shouldClaimBid: Map = new Map(); + let currencyDecimals = 18; + if (currencyToken !== null) { + currencyDecimals = await currencyToken.decimals(); + } + // Exit each bid first + for (const bid of bids) { + let balanceBefore = 0n; + if (currencyToken) { + balanceBefore = await currencyToken.balanceOf(owner); + } else { + balanceBefore = await hre.ethers.provider.getBalance(owner); + } + const bidId = bid.bidId; + const maxPrice = bid.maxPrice; + // Check if bid is above, at, or below final clearing price + if (maxPrice > finalCheckpoint.clearingPrice) { + // Bid is above final clearing - try simple exitBid first + try { + let tx = await auction.exitBid(bidId); + const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; + console.log(` โœ… Exited bid ${bidId} at block ${previousBlock} (simple exit - above clearing)`); + shouldClaimBid.set(bidId, true); + continue; // Successfully exited, move to next bid + } catch (error) { + // Simple exit failed, fall through to try partial exit + } + } + + // Try partial exit (for bids at/below clearing, or if simple exit failed) + try { + const hints = this.findCheckpointHints(maxPrice, sortedCheckpoints); + if (hints) { + let tx = await auction.exitPartiallyFilledBid(bidId, hints.lastFullyFilled, hints.outbid); + const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; + console.log( + ` โœ… Exited bid ${bidId} at block ${previousBlock} (partial exit with hints: ${hints.lastFullyFilled}, ${hints.outbid})`, + ); + const receipt = await hre.ethers.provider.getTransactionReceipt(tx.hash); + for (const log of receipt?.logs ?? []) { + const parsedLog = auction.interface.parseLog({ + topics: log.topics, + data: log.data, + }); + if (parsedLog?.name === EVENTS.BID_EXITED) { + if (parsedLog.args[2] > 0n) { + shouldClaimBid.set(bidId, true); + } + } + } + } else { + console.log(` โš ๏ธ Could not find checkpoint hints for bid ${bidId} (price: ${maxPrice})`); + } + } catch (partialExitError) { + const errorMsg = partialExitError instanceof Error ? partialExitError.message : String(partialExitError); + const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; + console.log(` โš ๏ธ Could not exit bid ${bidId} at block ${previousBlock}: ${errorMsg.substring(0, 200)}`); + } + let balanceAfter = 0n; + if (currencyToken) { + balanceAfter = await currencyToken.balanceOf(owner); + } else { + balanceAfter = await hre.ethers.provider.getBalance(owner); + } + const refundAmount = balanceAfter - balanceBefore; + calculatedCurrencyRaised += Number(bid.amount - refundAmount) / 10 ** currencyDecimals; + } + + // Claim all tokens in batch + const unfilteredBidIds = bids.map((b) => b.bidId); + const bidIds = unfilteredBidIds.filter((bidId) => shouldClaimBid.get(bidId)); + try { + await auction.claimTokensBatch(owner, bidIds); + console.log(` โœ… Claimed tokens for all bids`); + } catch (error) { + console.log(` โš ๏ธ Batch claim failed, trying individual claims...`); + // Fallback to individual claims + for (const bid of bids) { + const bidId = bid.bidId; + try { + await auction.claimTokens(bidId); + console.log(` โœ… Claimed tokens for bid ${bidId}`); + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e); + console.log(` โš ๏ธ Could not claim bid ${bidId}: ${errorMsg.substring(0, 200)}`); + } + } + } + } + + return calculatedCurrencyRaised; + } + + /** + * Finds checkpoint hints for exiting a partially filled bid + * @param bidMaxPrice - The maximum price of the bid + * @param checkpoints - Sorted array of checkpoints + * @returns Object with lastFullyFilled and outbid block numbers, or null if not applicable + */ + private findCheckpointHints( + bidMaxPrice: bigint, + checkpoints: CheckpointInfo[], + ): { lastFullyFilled: number; outbid: number } | null { + // Find lastFullyFilledCheckpoint: last checkpoint where clearingPrice < bidMaxPrice + let lastFullyFilledBlock = 0; + for (let i = checkpoints.length - 1; i >= 0; i--) { + if (checkpoints[i].clearingPrice < bidMaxPrice) { + lastFullyFilledBlock = checkpoints[i].blockNumber; + break; + } + } + + // Find outbidBlock: first checkpoint where clearingPrice > bidMaxPrice + let outbidBlock = 0; + for (let i = 0; i < checkpoints.length; i++) { + const cp = checkpoints[i]; + if (cp.clearingPrice > bidMaxPrice) { + outbidBlock = cp.blockNumber; + break; + } + } + + // If we found a fully filled checkpoint but no outbid (bid partially filled at end) + // or if we found both, return the hints + if (lastFullyFilledBlock > 0 || outbidBlock > 0) { + return { + lastFullyFilled: lastFullyFilledBlock, + outbid: outbidBlock, + }; + } + + return null; + } + + /** + * Tracks bidIds and checkpoints from events in transaction receipts + */ + private async trackBidIdsFromReceipts(receipts: any[], auction: Contract): Promise { + let checkpointCount = 0; + + for (const receipt of receipts) { + if (!receipt) continue; + + for (const log of receipt.logs) { + try { + const parsedLog = auction.interface.parseLog({ + topics: log.topics, + data: log.data, + }); + + if (parsedLog && parsedLog.name === EVENTS.BID_SUBMITTED) { + const bidId = Number(parsedLog.args[0]); // bidId + const owner = parsedLog.args[1]; // owner + const maxPrice = BigInt(parsedLog.args[2]); // maxPrice + const amount = BigInt(parsedLog.args[3]); // amount + + const bidInfo: BidInfo = { bidId, owner, maxPrice, amount }; + + if (!this.bidsByOwner.has(owner)) { + this.bidsByOwner.set(owner, []); + } + this.bidsByOwner.get(owner)!.push(bidInfo); + } else if (parsedLog && parsedLog.name === EVENTS.CHECKPOINT_UPDATED) { + const blockNumber = Number(parsedLog.args[0]); // blockNumber + const clearingPrice = BigInt(parsedLog.args[1]); // clearingPrice + + this.checkpoints.push({ blockNumber, clearingPrice }); + checkpointCount++; + } + } catch (error) { + // Log parsing failed, continue + continue; + } + } + } + + if (checkpointCount > 0) { + console.log(` ๐Ÿ“Š ${checkpointCount} checkpoint(s) created`); + } + } + + /** + * Handles initialization transactions for test setup. + * @param setupTransactions - Array of setup transactions to execute + */ + async handleInitializeTransactions(setupTransactions: TransactionInfo[]): Promise { + const provider = hre.ethers.provider; + + // Pause automine so all txs pile up + await provider.send(METHODS.EVM.SET_AUTOMINE, [false]); + await provider.send(METHODS.EVM.SET_INTERVAL_MINING, [0]); + + const nextNonce = new Map(); + + const fee = await hre.ethers.provider.getFeeData(); + const defaultTip = fee.maxPriorityFeePerGas ?? 1n * 10n ** 9n; // 1 gwei + const defaultMax = fee.maxFeePerGas ?? fee.gasPrice ?? 30n * 10n ** 9n; + + // enqueue all + for (const job of setupTransactions) { + const defaultFrom = await (await hre.ethers.getSigners())[0].getAddress(); + const from = hre.ethers.getAddress(job.from ?? defaultFrom); // canonical + + if (job.from) await provider.send(METHODS.HARDHAT.IMPERSONATE_ACCOUNT, [from]); + const signer = await hre.ethers.getSigner(from); + + const n = nextNonce.has(from) ? nextNonce.get(from)! : await signer.getNonce(PENDING_STATE); + + // base request + fees (donโ€™t rely on global defaults) + let req: TransactionRequest = { + ...job.tx, + nonce: n, + maxPriorityFeePerGas: job.tx.maxPriorityFeePerGas ?? defaultTip, + maxFeePerGas: job.tx.maxFeePerGas ?? defaultMax, + }; + + // Cap gas so multiple txs can fit in one block + if (!req.gasLimit) { + const est = await signer.estimateGas(req); + req.gasLimit = (est * 120n) / 100n; // +20% cushion + } + + await signer.sendTransaction(req); // enqueued; not mined + nextNonce.set(from, n + 1); + if (job.from) await provider.send(METHODS.HARDHAT.STOP_IMPERSONATING_ACCOUNT, [from]); + } + + // mine exactly one block + await provider.send(METHODS.EVM.MINE, []); + + await provider.send(METHODS.EVM.SET_AUTOMINE, [true]); + } + + /** + * Executes all events (bids, actions, assertions) with integrated validation. + * All events are collected and sorted by block to ensure proper chronological order. + * Assertions are validated at their specific blocks during execution. + * Multiple events in the same block are executed together. + * @param bidSimulator - The bid simulator instance + * @param assertionEngine - The assertion engine instance + * @param interactionData - Test interaction data containing bids, actions, and assertions + * @param remainingTransactions - Array of remaining transactions to execute + * @param createAuctionBlock - The block number when the auction was started + */ + async executeWithAssertions( + bidSimulator: BidSimulator, + assertionEngine: AssertionEngine, + interactionData: TestInteractionData, + remainingTransactions: TransactionInfo[], + createAuctionBlock: string, + offsetBlocks: number, + ): Promise { + // Collect all events (bids, actions, assertions) and sort by block + const allEvents: EventData[] = []; + + // Add bids using BidSimulator's collection logic + const allBids = bidSimulator.collectAllBids(interactionData); + allBids.forEach((bid) => { + allEvents.push({ + type: EventType.BID, + atBlock: bid.bidData.atBlock + offsetBlocks, + data: bid, + }); + }); + + // Add actions + if (interactionData.actions) { + interactionData.actions.forEach((action) => { + action.interactions.forEach((interactionGroup: AdminAction[] | TransferAction[]) => { + interactionGroup.forEach((interaction: AdminAction | TransferAction) => { + allEvents.push({ + type: EventType.ACTION, + atBlock: interaction.atBlock + offsetBlocks, + data: { actionType: action.type, ...interaction }, + }); + }); + }); + }); + } + + // Add assertions + if (interactionData.assertions) { + interactionData.assertions.forEach((checkpoint) => { + allEvents.push({ + type: EventType.ASSERTION, + atBlock: checkpoint.atBlock + offsetBlocks, + data: checkpoint, + }); + }); + } + const accumulatedTransactions: TransactionInfo[] = []; + // Sort all events by block number, then by type (transactions first, then assertions) + allEvents.sort((a, b) => { + if (a.atBlock !== b.atBlock) { + return a.atBlock - b.atBlock; + } + // Within the same block, execute transactions before assertions + const transactionTypes = [EventType.BID, EventType.ACTION]; + const aIsTransaction = transactionTypes.includes(a.type); + const bIsTransaction = transactionTypes.includes(b.type); + + if (aIsTransaction && !bIsTransaction) return -1; // a (transaction) comes first + if (!aIsTransaction && bIsTransaction) return 1; // b (transaction) comes first + return 0; // same type, maintain original order + }); + + // Group events by block number + const eventsByBlock: Record = {}; + allEvents.forEach((event) => { + if (!eventsByBlock[event.atBlock]) { + eventsByBlock[event.atBlock] = []; + } + eventsByBlock[event.atBlock].push(event); + }); + + const totalEvents = allEvents.length; + const totalBlocks = Object.keys(eventsByBlock).length; + console.log(LOG_PREFIXES.INFO, "Executing", totalEvents, "events across", totalBlocks, "blocks..."); + + // Execute events block by block + for (const [blockNumber, blockEvents] of Object.entries(eventsByBlock)) { + if (remainingTransactions.length > 0) { + if (blockNumber === createAuctionBlock.toString()) { + accumulatedTransactions.push(...remainingTransactions); + } else { + await this.handleTransactions(remainingTransactions, bidSimulator.getReverseLabelMap()); + } + remainingTransactions = []; + } + const blockNum = parseInt(blockNumber); + console.log(LOG_PREFIXES.INFO, "Block", blockNumber + ":", blockEvents.length, "event(s)"); + + // Separate transaction events from assertion events + const transactionEvents = blockEvents.filter((e) => e.type === EventType.BID || e.type === EventType.ACTION); + const assertionEvents = blockEvents.filter((e) => e.type === EventType.ASSERTION); + + // Mine to target block ONCE if there are transaction events + if (transactionEvents.length > 0) { + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const blocksToMine = blockNum - currentBlock - 1; // Mine to targetBlock - 1 + + if (blocksToMine > 0) { + await this.network.provider.send(METHODS.HARDHAT.MINE, [`0x${blocksToMine.toString(16)}`]); + } else if (blocksToMine < 0) { + console.log(LOG_PREFIXES.WARNING, "Block", blockNum, "is already mined. Current block:", currentBlock); + throw new Error(ERROR_MESSAGES.BLOCK_ALREADY_MINED(blockNum)); + } + + // Execute all transaction events in this block + for (const event of transactionEvents) { + console.log(LOG_PREFIXES.INFO, " -", event.type); + switch (event.type) { + case EventType.BID: + await bidSimulator.executeBid(event.data as InternalBidData, accumulatedTransactions); + break; + case EventType.ACTION: + const actionData = event.data as ActionData; + if (actionData.actionType === ActionType.TRANSFER_ACTION) { + await bidSimulator.executeTransfers([actionData as TransferAction], accumulatedTransactions); + } else if (actionData.actionType === ActionType.ADMIN_ACTION) { + await bidSimulator.executeAdminActions([actionData as AdminAction], accumulatedTransactions); + } + break; + } + } + // Mine the block with all transactions + await this.handleTransactions(accumulatedTransactions, bidSimulator.getReverseLabelMap()); + } + + // Handle assertion events (after transactions are mined) + if (assertionEvents.length > 0) { + // Mine to target block if we haven't already (no transactions case) + if (transactionEvents.length === 0) { + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const blocksToMine = blockNum - currentBlock; + + if (blocksToMine > 0) { + await this.network.provider.send(METHODS.HARDHAT.MINE, [`0x${blocksToMine.toString(16)}`]); + } else if (blocksToMine < 0) { + console.log(LOG_PREFIXES.WARNING, "Block", blockNum, "is already mined. Current block:", currentBlock); + throw new Error(ERROR_MESSAGES.BLOCK_ALREADY_MINED(blockNum)); + } + } + + // Execute all assertion events in this block + for (const event of assertionEvents) { + console.log(LOG_PREFIXES.INFO, " -", event.type); + const assertionData = event.data as AssertionInfo; + console.log(LOG_PREFIXES.INFO, "Validating assertion:", assertionData.reason); + await assertionEngine.validateAssertion(assertionData.assert); + console.log(LOG_PREFIXES.SUCCESS, "Assertion validated"); + } + } + } + } + + /** + * Handles transaction execution with same-block support and revert validation. + * @param transactions - Array of transactions to execute + */ + async handleTransactions(transactions: TransactionInfo[], reverseLabelMap: Map): Promise { + const provider = hre.network.provider; + + // Pause automining so txs pile up in the mempool + await provider.send(METHODS.EVM.SET_AUTOMINE, [false]); + await provider.send(METHODS.EVM.SET_INTERVAL_MINING, [0]); // ensure no timer mines a block + + const pendingHashes: HashWithRevert[] = []; + const nextNonce = new Map(); + + try { + await this.submitTransactions(transactions, pendingHashes, nextNonce, reverseLabelMap, provider); + + // Mine exactly one block containing all pending txs + await provider.send(METHODS.EVM.MINE, []); + + const receipts = await this.validateReceipts(pendingHashes); + + // Track bidIds from BidSubmitted events + if (this.auction && receipts.length > 0) { + await this.trackBidIdsFromReceipts(receipts, this.auction); + } + } finally { + // Restore automining + await provider.send(METHODS.EVM.SET_AUTOMINE, [true]); + } + } + + /** + * Submits transactions to the network and tracks them for validation. + * @param transactions - Array of transactions to submit + * @param pendingHashes - Array to track transaction hashes with revert information + * @param nextNonce - Map of addresses to their next nonce values + * @param provider - The network provider instance + */ + private async submitTransactions( + transactions: TransactionInfo[], + pendingHashes: HashWithRevert[], + nextNonce: Map, + reverseLabelMap: Map, + provider: any, + ): Promise { + while (transactions.length > 0) { + const txInfo = transactions.shift()!; + let from = txInfo.from as string; + + if (from) { + let fromLabel = reverseLabelMap.get(from); + if (fromLabel) { + console.log(LOG_PREFIXES.INFO, "From:", from, `(${fromLabel})`); + } else { + console.log(LOG_PREFIXES.INFO, "From:", from); + } + await provider.send(METHODS.HARDHAT.IMPERSONATE_ACCOUNT, [from]); + } else { + const defaultFrom = await (await hre.ethers.getSigners())[0].getAddress(); + from = hre.ethers.getAddress(defaultFrom); // canonical + } + + const signer = await hre.ethers.getSigner(from); + + // maintain nonce continuity per sender (using "pending" base) + const n = nextNonce.has(from) ? nextNonce.get(from)! : await signer.getNonce(PENDING_STATE); + const req = { ...txInfo.tx, nonce: n }; + try { + const resp = await signer.sendTransaction(req); // just enqueues; not mined + pendingHashes.push({ hash: resp.hash, expectRevert: txInfo.expectRevert }); + nextNonce.set(from, n + 1); + } catch (error: any) { + let matched = false; + if (txInfo.expectRevert) { + const errorMessage = error?.message || error?.toString() || ""; + if (txInfo.expectRevert && errorMessage.toLowerCase().includes(txInfo.expectRevert.toLowerCase())) { + matched = true; + console.log(LOG_PREFIXES.SUCCESS, `Expected revert caught: ${txInfo.expectRevert}`); + } + } + if (!matched) { + throw new Error( + ERROR_MESSAGES.EXPECTED_REVERT_MISMATCH( + txInfo.expectRevert || "none", + error instanceof Error ? error.message : String(error), + ), + ); + } + } + + if (from) await provider.send(METHODS.HARDHAT.STOP_IMPERSONATING_ACCOUNT, [from]); + if (txInfo.msg) console.log(LOG_PREFIXES.TRANSACTION, txInfo.msg); + } + } + + /** + * Validates transaction receipts and checks for expected reverts. + * @param pendingHashes - Array of transaction hashes with expected revert information + * @returns Array of receipts for further processing + */ + private async validateReceipts(pendingHashes: HashWithRevert[]): Promise { + // Collect receipts (all from the same block) + const receipts = await Promise.all(pendingHashes.map((h) => hre.ethers.provider.getTransactionReceipt(h.hash))); + + for (const pendingHash of pendingHashes) { + const receipt = receipts.find((r) => r?.hash === pendingHash.hash); + if (receipt) { + let decodedRevert: string = "transaction succeeded"; + if (parseBoolean(pendingHash.expectRevert ?? "")) { + // Status 0 means revert + if (receipt.status === 0) { + console.log(LOG_PREFIXES.SUCCESS, `Expected revert caught: ${pendingHash.expectRevert}`); + continue; + } + } else if (pendingHash.expectRevert) { + // Status 0 means revert + if (receipt.status === 0) { + decodedRevert = await this.decodeRevert(pendingHash.hash); + if (decodedRevert.toLowerCase().includes(pendingHash.expectRevert.toLowerCase())) { + console.log(LOG_PREFIXES.SUCCESS, `Expected revert caught: ${pendingHash.expectRevert}`); + continue; + } + } + } else { + continue; + } + throw new Error(ERROR_MESSAGES.EXPECTED_REVERT_MISMATCH(pendingHash.expectRevert || "none", decodedRevert)); + } + } + + const blockNumber = receipts[0]?.blockNumber; + if (receipts.length > 0) { + console.log(LOG_PREFIXES.SUCCESS, "Mined", receipts.length, "txs in block", blockNumber); + } + + return receipts.filter((r) => r !== null); + } + + /** + * Builds an ethers.js Interface for decoding project-specific errors. + * @returns Interface containing all project error definitions + */ + async getProjectErrorInterface(): Promise { + if (this.cachedInterface) return this.cachedInterface; + + const fqns = await artifacts.getAllFullyQualifiedNames(); + + // Collect JSON ABI items for all custom errors + const allErrorItems: string[] = [ + // Also include the built-ins as strings + "error Error(string)", + "error Panic(uint256)", + ]; + + const seen = new Set(); // dedupe by signature + + for (const fqn of fqns) { + const art = await artifacts.readArtifact(fqn); + for (const item of art.abi) { + if (item.type === TYPES.ERROR) { + // Build a signature + const sig = `${item.name}(` + (item.inputs ?? []).map((i: any) => i.type).join(",") + `)`; + if (seen.has(sig)) continue; + seen.add(sig); + allErrorItems.push(item); + } + } + } + + this.cachedInterface = new hre.ethers.Interface(allErrorItems); + return this.cachedInterface!; + } + + /** + * Decodes revert reason from a failed transaction. + * @param hash - The transaction hash to decode + * @returns The decoded revert reason + */ + private async decodeRevert(hash: string): Promise { + const trace = await hre.network.provider.send(METHODS.DEBUG.TRACE_TRANSACTION, [ + hash, + { disableStorage: true, disableMemory: true, disableStack: false }, + ]); + let data = trace.returnValue; + if (data.slice(0, 2) !== "0x") data = "0x" + data; + const iface = await this.getProjectErrorInterface(); + try { + const desc = iface.parseError(data); + if (desc == null) return ""; + return desc.name + "(" + desc.args.join(",") + ")"; + } catch { + if (!data || data === "0x") return ""; + return "Unknown error"; + } + } + + /** + * Resets the blockchain and deploys Permit2 at the canonical address. + */ + private async resetChainWithPermit2(): Promise { + await hre.network.provider.send(METHODS.HARDHAT.RESET); + // Check if Permit2 is already deployed at the canonical address + const code = await hre.ethers.provider.getCode(PERMIT2_ADDRESS); + + if (code === "0x") { + console.log(LOG_PREFIXES.INFO, "Deploying Permit2 at canonical address..."); + + // Load the Permit2 artifact + // TODO: find a way to avoid require + const permit2Artifact = require("../../../lib/permit2/out/Permit2.sol/Permit2.json"); + + // Deploy Permit2 using the factory pattern first, then move to canonical address + const permit2Factory = await hre.ethers.getContractFactory(permit2Artifact.abi, permit2Artifact.bytecode.object); + + // Deploy Permit2 normally first + const permit2Contract = await permit2Factory.deploy(); + await permit2Contract.waitForDeployment(); + const permit2Address = await permit2Contract.getAddress(); + + // Get the deployed bytecode + const deployedCode = await hre.ethers.provider.getCode(permit2Address); + await hre.network.provider.send(METHODS.HARDHAT.RESET); + // Set code at canonical address + await hre.network.provider.send(METHODS.HARDHAT.SET_CODE, [PERMIT2_ADDRESS, deployedCode]); + + console.log(LOG_PREFIXES.SUCCESS, "Permit2 deployed at canonical address"); + } else { + console.log(LOG_PREFIXES.SUCCESS, "Permit2 already deployed at canonical address"); + } + } +} diff --git a/test/e2e/src/constants.ts b/test/e2e/src/constants.ts new file mode 100644 index 00000000..0ec700e6 --- /dev/null +++ b/test/e2e/src/constants.ts @@ -0,0 +1,146 @@ +/** + * Configuration constants for E2E tests + * Centralizes hardcoded values for better maintainability + */ +import { Address } from "../schemas/TestSetupSchema"; + +// Network and blockchain constants +export const NATIVE_CURRENCY_ADDRESS = "0x0000000000000000000000000000000000000000" as Address; +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as Address; +export const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3" as Address; +export const UINT_160_MAX = "0xffffffffffffffffffffffffffffffffffffffff"; +export const UINT_48_MAX = "0xffffffffffff"; + +// ERC20 constants +export const MAX_UINT256 = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + +// Hardhat network methods +export const HARDHAT_METHODS = { + MINE: "hardhat_mine", + IMPERSONATE_ACCOUNT: "hardhat_impersonateAccount", + STOP_IMPERSONATING_ACCOUNT: "hardhat_stopImpersonatingAccount", + SET_BALANCE: "hardhat_setBalance", +} as const; + +// True constants - values that never change +export const MPS = 10000000; // 1e7 - Million Price Steps +export const MAX_SYMBOL_LENGTH = 4; +export const HEX_PADDING_LENGTH = 16; +export const DEFAULT_TOTAL_SUPPLY = "1000000000000000000000"; // 1000 tokens with 18 decimals +// Error messages +export const ERROR_MESSAGES = { + // AuctionDeployer errors + AUCTIONED_TOKEN_NOT_FOUND: (tokenName: string) => `Auctioned token ${tokenName} not found`, + AUCTION_PARAMETERS_NOT_FOUND: "AuctionParameters struct not found in auction artifact", + AUCTION_FACTORY_NOT_DEPLOYED: "AuctionFactory not deployed. Call initialize() first.", + AUCTION_DEPLOYER_NOT_INITIALIZED: "AuctionDeployer not initialized. Call initialize() first.", + AUCTION_CREATION_FAILED: (errorMessage: string) => `Auction creation failed. Original error: ${errorMessage}`, + TOKEN_NOT_FOUND: (tokenName: string) => `Token ${tokenName} not found`, + AUCTION_NOT_DEPLOYED: "Auction not deployed. Call createAuction() first.", + AUCTIONED_TOKEN_IS_NULL: "Auctioned token is null", + + // AssertionEngine errors + TOKEN_IDENTIFIER_NOT_FOUND: (tokenIdentifier: string) => `Token with identifier ${tokenIdentifier} not found.`, + CANNOT_VALIDATE_EQUALITY: "Can only validate equality for non-object types", + TOKEN_NOT_FOUND_BY_ADDRESS: (tokenAddress: string) => `Token not found for address: ${tokenAddress}`, + TOTAL_SUPPLY_ASSERTION_FAILED: (expected: string, actual: string) => + `Total supply assertion failed: expected ${expected}, got ${actual}`, + AUCTION_ASSERTION_FAILED: (expected: any, actual: any, field?: string, variance?: string) => + variance + ? `Auction assertion failed${field ? ` for ${field}` : ""}. Expected ${expected} ยฑ ${variance}, got ${actual}` + : `Auction assertion failed${field ? ` for ${field}` : ""}. Expected ${expected}, got ${actual}`, + AUCTION_CHECKPOINT_ASSERTION_FAILED: (expected: any, actual: any, field?: string, variance?: string) => + variance + ? `Auction latestCheckpoint assertion failed${ + field ? ` for ${field}` : "" + }. Expected ${expected} ยฑ ${variance}, got ${actual}` + : `Auction latestCheckpoint assertion failed${field ? ` for ${field}` : ""}. Expected ${expected}, got ${actual}`, + BLOCK_NOT_FOUND: (currentBlock: number) => `Block ${currentBlock} not found`, + EVENT_ASSERTION_FAILED: (eventName: string) => + `Event assertion failed: Event '${eventName}' not found with expected arguments`, + BALANCE_ASSERTION_FAILED: (address: string, token: string, expected: string, actual: string, variance?: string) => + variance + ? `Balance assertion failed for ${address} token ${token}. Expected ${expected} ยฑ ${variance}, got ${actual}` + : `Balance assertion failed for ${address} token ${token}. Expected ${expected}, got ${actual}`, + + // BidSimulator errors + PERCENT_OF_SUPPLY_INVALID_SIDE: + "PERCENT_OF_SUPPLY can only be used for auctioned token (OUTPUT), not currency (INPUT)", + EXPECTED_REVERT_NOT_FOUND: (expected: string, actual: string) => + `Expected revert data to contain "${expected}", but got: ${actual}`, + PREVIOUS_TICK_OR_TICK_PRICE_NOT_PROVIDED: "previousTick or prevTickPrice must be provided", + + // E2ECliRunner errors + NO_INSTANCE_FOUND: (filePath: string) => `No instance found in ${filePath}`, + FAILED_TO_LOAD_FILE: (filePath: string, error: string) => `Failed to load ${filePath}: ${error}`, + + // SingleTestRunner errors + BLOCK_ALREADY_PAST_START: (currentBlock: number, auctionStartBlock: number) => + `Current block ${currentBlock} is already past auction start block ${auctionStartBlock}`, + BLOCK_ALREADY_MINED: (blockNum: number) => `Block ${blockNum} is already mined`, + EXPECTED_REVERT_MISMATCH: (expected: string, actual: string) => `Expected revert "${expected}" but got: ${actual}`, + + // SchemaValidator errors + TYPESCRIPT_FILE_NOT_FOUND: (tsFilePath: string) => `TypeScript test instance file not found: ${tsFilePath}`, + NO_EXPORT_FOUND: (filename: string, availableExports: string) => + `No export found in ${filename}.ts. Available exports: ${availableExports}`, + FAILED_TO_LOAD_TYPESCRIPT_INSTANCE: (filename: string, error: string) => + `Failed to load TypeScript instance ${filename}: ${error}`, +} as const; + +// Logging prefixes +export const LOG_PREFIXES = { + RUN: " ๐Ÿš€", + DEPLOY: " ๐Ÿช™", + SUCCESS: " โœ…", + ERROR: " โŒ", + WARNING: " โš ๏ธ", + INFO: " ๐Ÿ”", + DEBUG: " ๐Ÿ›", + TRANSACTION: " ๐Ÿ”ธ", + ASSERTION: " ๐Ÿ’ฐ", + CONFIG: " ๐Ÿ“Š", + AUCTION: " ๐Ÿ›๏ธ", + PHASE: "๐ŸŽฏ", + TEST: "๐Ÿงช", + FINAL: "๐ŸŽ‰", + NOTE: "๐Ÿ’ก", + FILES: "๐Ÿ“", +} as const; + +export enum TYPES { + OBJECT = "object", + ERROR = "error", +} + +export const METHODS = { + HARDHAT: { + IMPERSONATE_ACCOUNT: "hardhat_impersonateAccount", + STOP_IMPERSONATING_ACCOUNT: "hardhat_stopImpersonatingAccount", + SET_BALANCE: "hardhat_setBalance", + MINE: "hardhat_mine", + RESET: "hardhat_reset", + SET_CODE: "hardhat_setCode", + }, + DEBUG: { + TRACE_TRANSACTION: "debug_traceTransaction", + }, + EVM: { + SET_AUTOMINE: "evm_setAutomine", + SET_INTERVAL_MINING: "evm_setIntervalMining", + MINE: "evm_mine", + }, +}; + +export const TYPE_FIELD = "type"; +export const PENDING_STATE = "pending"; +export const SETUP = "setup"; +export const INTERACTION = "interaction"; + +export const NATIVE_CURRENCY_NAME = "Native Currency"; + +export const EVENTS = { + BID_SUBMITTED: "BidSubmitted", + CHECKPOINT_UPDATED: "CheckpointUpdated", + BID_EXITED: "BidExited", +}; diff --git a/test/e2e/src/types.ts b/test/e2e/src/types.ts new file mode 100644 index 00000000..e75bb74c --- /dev/null +++ b/test/e2e/src/types.ts @@ -0,0 +1,129 @@ +import { Contract, ContractTransaction, TransactionLike } from "ethers"; +import { Address } from "../schemas/TestSetupSchema"; + +// Import contract artifacts to get proper typing +import auctionArtifact from "../../../out/Auction.sol/Auction.json"; +import auctionFactoryArtifact from "../../../out/AuctionFactory.sol/AuctionFactory.json"; +import mockTokenArtifact from "../../../out/WorkingCustomMockToken.sol/WorkingCustomMockToken.json"; +import { ActionType, AdminAction, AssertionInfo, TransferAction } from "../schemas/TestInteractionSchema"; +import { InternalBidData } from "./BidSimulator"; + +// Contract types with proper ABI-based typing +export type AuctionContract = Contract & { + interface: typeof auctionArtifact.abi; +}; +export type AuctionFactoryContract = Contract & { + interface: typeof auctionFactoryArtifact.abi; +}; +export type TokenContract = Contract & { + interface: typeof mockTokenArtifact.abi; +}; + +// Event data types +export interface BidEventData { + bidData: { + atBlock: number; + amount: { + side: "input" | "output"; + type: "raw" | "percentOfSupply" | "basisPoints" | "percentOfGroup"; + value: string; + variation?: string; + token?: Address | string; + }; + price: { + type: "raw" | "tick"; + value: string; + variation?: string; + }; + previousTick: number; + hookData?: string; + expectRevert?: string; + }; + bidder: string; + type: "named" | "group"; + group?: string; +} + +export interface ActionEventData { + actionType: "AdminAction" | "TransferAction"; + atBlock: number; + method?: string; + value?: { + from: Address; + to: Address | string; + token: Address | string; + amount: string; + }; +} + +export interface AssertionEventData { + atBlock: number; + reason: string; + assert: { + type: "balance" | "totalSupply" | "event" | "pool"; + address?: Address; + token?: Address | string; + expected?: string; + eventName?: string; + expectedArgs?: Record; + tick?: string; + sqrtPriceX96?: string; + liquidity?: string; + }; +} + +// Test result types +export interface TestExecutionResult { + success: boolean; + error?: string; + duration?: number; +} + +export interface TestCombinationResult { + setupFile: string; + interactionFile: string; + result: TestExecutionResult; +} + +// Configuration types +export interface AuctionConfig { + currency: Address; + tokensRecipient: Address; + fundsRecipient: Address; + startBlock: number; + endBlock: number; + claimBlock: number; + tickSpacing: number | bigint | string; // number for small values, bigint/string for large values + validationHook: Address; + floorPrice: string; + auctionStepsData: string; +} + +export interface TransactionInfo { + tx: ContractTransaction | TransactionLike; + from: string | null; + msg?: string; + expectRevert?: string; +} + +export interface HashWithRevert { + hash: string; + expectRevert?: string; +} + +export enum EventType { + BID = "bid", + ACTION = "action", + ASSERTION = "assertion", +} + +export type ActionData = { actionType: ActionType } & (AdminAction | TransferAction); + +// Union type for all possible event data types +export type EventInternalData = InternalBidData | ActionData | AssertionInfo; + +export interface EventData { + type: EventType; + atBlock: number; + data: EventInternalData; +} diff --git a/test/e2e/src/utils.ts b/test/e2e/src/utils.ts new file mode 100644 index 00000000..ec162133 --- /dev/null +++ b/test/e2e/src/utils.ts @@ -0,0 +1,82 @@ +import { PriceConfig, PriceType } from "../schemas/TestInteractionSchema"; +import { ERROR_MESSAGES } from "./constants"; +import { AuctionDeployer } from "./AuctionDeployer"; + +/** + * Resolves a token identifier to its contract address. + * @param tokenIdentifier - Token name or address (if starts with "0x", returns as-is) + * @returns The resolved token address + * @throws Error if token is not found + */ +export async function resolveTokenAddress(tokenIdentifier: string, auctionDeployer: AuctionDeployer): Promise { + if (tokenIdentifier.startsWith("0x")) { + return tokenIdentifier; // It's already an address + } + // Look up by name in the deployed tokens (from AuctionDeployer) + const tokenContract = auctionDeployer.getTokenByName(tokenIdentifier); + if (tokenContract) { + return await tokenContract.getAddress(); + } + + throw new Error(ERROR_MESSAGES.TOKEN_IDENTIFIER_NOT_FOUND(tokenIdentifier)); +} + +/** + * Converts a tick number to a price in X96 format. + * @param tickNumber - The tick number to convert + * @param floorPrice - The auction's floor price (already in Q96 format) + * @param tickSpacing - The auction's tick spacing + * @returns The price in X96 format as a bigint + */ +export function tickNumberToPriceX96(tickNumber: number, floorPrice: bigint, tickSpacing: bigint): bigint { + // Floor price is already in Q96, tick spacing is raw + // price = floorPrice + (tickNumber - 1) * tickSpacing + return floorPrice + (BigInt(tickNumber) - 1n) * tickSpacing; +} + +/** + * Calculates the bid price based on the price configuration. + * @param priceConfig - Price configuration specifying type and value + * @returns The calculated price as a bigint + * @throws Error if price type is unsupported + */ +export async function calculatePrice( + priceConfig: PriceConfig, + floorPrice?: bigint, + tickSpacing?: bigint, +): Promise { + let value: bigint; + + if (priceConfig.type === PriceType.TICK) { + // Convert tick to actual price using the auction's parameters + if (!floorPrice || !tickSpacing) { + throw new Error("floorPrice and tickSpacing required for TICK price type"); + } + value = tickNumberToPriceX96(parseInt(priceConfig.value.toString()), floorPrice, tickSpacing); + } else { + // Ensure the value is treated as a string to avoid scientific notation conversion + value = BigInt(priceConfig.value.toString()); + } + + // Implement price variation + if (priceConfig.variation) { + const variationPercent = parseFloat(priceConfig.variation); + const variationAmount = (Number(value) * variationPercent) / 100; + const randomVariation = (Math.random() - 0.5) * 2 * variationAmount; // -variation to +variation + const adjustedValue = Number(value) + randomVariation; + + // Ensure the price doesn't go negative + value = BigInt(Math.max(0, Math.floor(adjustedValue))); + } + + return value; +} + +/** + * Checks if a string is any possible stringified version of a true boolean value. + * @param value - The string to parse + * @returns True if the string is a stringified version of a true boolean value, false otherwise + */ +export function parseBoolean(value: string): boolean { + return value.toLowerCase() === "true" || value === "1"; +} diff --git a/test/e2e/tests/e2e.test.ts b/test/e2e/tests/e2e.test.ts new file mode 100644 index 00000000..b46ea4d9 --- /dev/null +++ b/test/e2e/tests/e2e.test.ts @@ -0,0 +1,104 @@ +import { MultiTestRunner } from "../src/MultiTestRunner"; +import { simpleSetup } from "../instances/setup/SimpleSetup"; +import { simpleInteraction } from "../instances/interaction/SimpleInteraction"; +import { erc20Setup } from "../instances/setup/ERC20Setup"; +import { erc20Interaction } from "../instances/interaction/ERC20Interaction"; +import { advancedSetup } from "../instances/setup/AdvancedSetup"; +import { advancedInteraction } from "../instances/interaction/AdvancedInteraction"; +import { variationSetup } from "../instances/setup/VariationSetup"; +import { variationInteraction } from "../instances/interaction/VariationInteraction"; +import { extendedSetup } from "../instances/setup/ExtendedSetup"; +import { extendedInteraction } from "../instances/interaction/ExtendedInteraction"; +import { TestSetupData } from "../schemas/TestSetupSchema"; +import { TestInteractionData } from "../schemas/TestInteractionSchema"; +import { CombinationResult } from "../src/MultiTestRunner"; +import hre from "hardhat"; + +describe("E2E Tests", function () { + let runner: MultiTestRunner; + let results: CombinationResult[] = []; + + before(async function () { + console.log("๐Ÿ” Test: hre.ethers available?", !!hre.ethers); + runner = new MultiTestRunner(); + }); + + it("should run simple setup and interaction", async function () { + const combinations = [{ setup: simpleSetup, interaction: simpleInteraction }]; + + await runTest(combinations); + }); + + it("should run erc20 setup and interaction", async function () { + const combinations = [{ setup: erc20Setup, interaction: erc20Interaction }]; + + await runTest(combinations); + }); + + it("should run advanced setup and interaction", async function () { + const combinations = [{ setup: advancedSetup, interaction: advancedInteraction }]; + + await runTest(combinations); + }); + + it("should run variation setup and interaction (with random amounts and variance assertions)", async function () { + const combinations = [{ setup: variationSetup, interaction: variationInteraction }]; + + await runTest(combinations); + }); + + /* + it("should run extended testing parameters", async function () { + this.timeout(30000); // Longer timeout for large auction + const combinations = [{ setup: extendedSetup, interaction: extendedInteraction }]; + + await runTest(combinations); + }); + */ + + after(function () { + printResults(); + }); + + async function runTest(combinations: { setup: TestSetupData; interaction: TestInteractionData }[]) { + console.log("๐Ÿš€ TWAP Auction E2E Test Runner"); + console.log("================================"); + + // Run the specified combinations + console.log(`\n๐ŸŽฏ Running ${combinations.length} specified combinations...`); + const _results = await runner.runAllCombinations(combinations); + results.push(..._results); + const failed = _results.filter((r) => !r.success).length; + if (failed > 0) { + console.log("\nโŒ Failed combinations:"); + _results + .filter((r) => !r.success) + .forEach((r) => { + console.log(` - ${r.setupName} + ${r.interactionName}: ${r.error}`); + }); + throw new Error(`${failed} test(s) failed`); + } + } + + function printResults() { + // Summary + const passed = results.filter((r) => r.success).length; + const failed = results.filter((r) => !r.success).length; + console.log("\n๐Ÿ Final Summary:"); + console.log(` Total combinations: ${results.length}`); + console.log(` โœ… Passed: ${passed}`); + console.log(` โŒ Failed: ${failed}`); + + if (failed > 0) { + console.log("\nโŒ Failed combinations:"); + results + .filter((r) => !r.success) + .forEach((r) => { + console.log(` - ${r.setupName} + ${r.interactionName}: ${r.error}`); + }); + throw new Error(`${failed} test(s) failed`); + } else { + console.log("\n๐ŸŽ‰ All tests passed!"); + } + } +}); From b7df4ef7cf014cbb0683a666f3ca281da60bdeb4 Mon Sep 17 00:00:00 2001 From: ericneil-sanc Date: Tue, 21 Oct 2025 13:51:00 -0400 Subject: [PATCH 2/5] resolve rebase conflicts --- test/e2e/schemas/TestSetupSchema.ts | 1 + test/e2e/src/AuctionDeployer.ts | 3 ++- test/e2e/src/SingleTestRunner.ts | 1 - test/e2e/src/types.ts | 3 ++- test/utils/MockToken.sol | 17 +++++++++++++++++ tsconfig.json | 24 ++++++++++++++++++++++++ 6 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tsconfig.json diff --git a/test/e2e/schemas/TestSetupSchema.ts b/test/e2e/schemas/TestSetupSchema.ts index b4723d21..868574cf 100644 --- a/test/e2e/schemas/TestSetupSchema.ts +++ b/test/e2e/schemas/TestSetupSchema.ts @@ -50,6 +50,7 @@ export interface AuctionParameters { tickSpacing: number | bigint | string; // number for small values, bigint/string for large values validationHook: Address; floorPrice: string; + requiredCurrencyRaised?: string; // Optional: minimum currency that must be raised auctionStepsData?: string | StepData[]; // Optional: raw hex string or array of steps } diff --git a/test/e2e/src/AuctionDeployer.ts b/test/e2e/src/AuctionDeployer.ts index 326f7740..d2749fbb 100644 --- a/test/e2e/src/AuctionDeployer.ts +++ b/test/e2e/src/AuctionDeployer.ts @@ -1,5 +1,5 @@ import { TestSetupData, Address, AdditionalToken, StepData } from "../schemas/TestSetupSchema"; -import mockTokenArtifact from "../../../out/WorkingCustomMockToken.sol/WorkingCustomMockToken.json"; +import mockTokenArtifact from "../../../out/MockToken.sol/WorkingCustomMockToken.json"; import auctionArtifact from "../../../out/Auction.sol/Auction.json"; import auctionFactoryArtifact from "../../../out/AuctionFactory.sol/AuctionFactory.json"; import { AuctionParametersStruct } from "../../../typechain-types/out/Auction"; @@ -382,6 +382,7 @@ export class AuctionDeployer { tickSpacing: config.tickSpacing, validationHook: config.validationHook, floorPrice: config.floorPrice, + requiredCurrencyRaised: config.requiredCurrencyRaised || 0, auctionStepsData: config.auctionStepsData, }; diff --git a/test/e2e/src/SingleTestRunner.ts b/test/e2e/src/SingleTestRunner.ts index 2ede84b7..a63db780 100644 --- a/test/e2e/src/SingleTestRunner.ts +++ b/test/e2e/src/SingleTestRunner.ts @@ -243,7 +243,6 @@ export class SingleTestRunner { console.log(" Currency raised:", finalState.currencyRaised); console.log(" Latest checkpoint:"); console.log(" Clearing price:", finalState.latestCheckpoint.clearingPrice); - console.log(" Currency raised (Q96_X7):", finalState.latestCheckpoint.currencyRaisedQ96_X7); console.log( " Currency raised at clearing price (Q96_X7):", finalState.latestCheckpoint.currencyRaisedAtClearingPriceQ96_X7, diff --git a/test/e2e/src/types.ts b/test/e2e/src/types.ts index e75bb74c..7f945665 100644 --- a/test/e2e/src/types.ts +++ b/test/e2e/src/types.ts @@ -4,7 +4,7 @@ import { Address } from "../schemas/TestSetupSchema"; // Import contract artifacts to get proper typing import auctionArtifact from "../../../out/Auction.sol/Auction.json"; import auctionFactoryArtifact from "../../../out/AuctionFactory.sol/AuctionFactory.json"; -import mockTokenArtifact from "../../../out/WorkingCustomMockToken.sol/WorkingCustomMockToken.json"; +import mockTokenArtifact from "../../../out/MockToken.sol/WorkingCustomMockToken.json"; import { ActionType, AdminAction, AssertionInfo, TransferAction } from "../schemas/TestInteractionSchema"; import { InternalBidData } from "./BidSimulator"; @@ -96,6 +96,7 @@ export interface AuctionConfig { tickSpacing: number | bigint | string; // number for small values, bigint/string for large values validationHook: Address; floorPrice: string; + requiredCurrencyRaised?: string; // Optional: minimum currency that must be raised auctionStepsData: string; } diff --git a/test/utils/MockToken.sol b/test/utils/MockToken.sol index de3923f7..c35b5e93 100644 --- a/test/utils/MockToken.sol +++ b/test/utils/MockToken.sol @@ -11,3 +11,20 @@ contract MockToken is ERC20ReturnFalseMock { _mint(account, amount); } } + +contract WorkingCustomMockToken is ERC20 { + uint8 private _decimals; + + constructor(string memory name, string memory symbol, uint8 decimals_, uint256 initialSupply) ERC20(name, symbol) { + _decimals = decimals_; + _mint(msg.sender, initialSupply); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..4b9f9ee3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "NodeNext", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["node", "mocha", "chai", "@nomicfoundation/hardhat-ethers"] + }, + "include": ["test/e2e/src/**/*", "test/e2e/tests/**/*", "script/**/*"], + "exclude": ["node_modules", "dist", "out", "lib", "cache"] +} From 375b853b13f9b218c0fe1a6749d3a85a59f7cd14 Mon Sep 17 00:00:00 2001 From: Eric Sanchirico Date: Thu, 23 Oct 2025 17:47:08 -0400 Subject: [PATCH 3/5] fix pr comments --- hardhat.config.ts | 5 - package.json | 3 - .../interaction/AdvancedInteraction.ts | 3 +- .../interaction/VariationInteraction.ts | 6 +- test/e2e/instances/setup/AdvancedSetup.ts | 3 +- test/e2e/instances/setup/ERC20Setup.ts | 3 +- test/e2e/instances/setup/ExtendedSetup.ts | 7 +- test/e2e/instances/setup/SimpleSetup.ts | 9 +- test/e2e/instances/setup/VariationSetup.ts | 11 +- test/e2e/schemas/TestInteractionSchema.ts | 1 - test/e2e/schemas/TestSetupSchema.ts | 6 +- test/e2e/src/AssertionEngine.ts | 206 ++++++++++++------ test/e2e/src/AuctionDeployer.ts | 10 +- test/e2e/src/BidSimulator.ts | 11 +- test/e2e/src/SingleTestRunner.ts | 24 +- test/e2e/src/constants.ts | 7 +- test/e2e/src/types.ts | 6 +- test/e2e/src/utils.ts | 14 +- 18 files changed, 205 insertions(+), 130 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 7cbb5173..4d413ab5 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -21,17 +21,12 @@ const config: HardhatUserConfig = { version: "0.8.26", settings: { optimizer: { enabled: true, runs: 200 }, - viaIR: true, // Required for complex contracts }, }, networks: { hardhat: { blockGasLimit: 40_000_000, accounts: { - count: 20, - initialIndex: 0, - mnemonic: "test test test test test test test test test test test junk", - path: "m/44'/60'/0'/0", accountsBalance: "10000000000000000000000", // 10k ETH }, }, diff --git a/package.json b/package.json index b903d31b..3cad907e 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,6 @@ "main": "index.js", "scripts": { "test": "hardhat test", - "test:setup": "hardhat test --grep 'Setup'", - "test:interaction": "hardhat test --grep 'Interaction'", - "test:combined": "hardhat test --grep 'Combined'", "test:all": "hardhat test", "e2e": "forge build && npm run typechain && hardhat test test/e2e/tests/e2e.test.ts", "e2e:run": "forge build && npm run typechain && npx ts-node test/e2e/src/E2ECliRunner.ts", diff --git a/test/e2e/instances/interaction/AdvancedInteraction.ts b/test/e2e/instances/interaction/AdvancedInteraction.ts index e87a3b0d..d636a058 100644 --- a/test/e2e/instances/interaction/AdvancedInteraction.ts +++ b/test/e2e/instances/interaction/AdvancedInteraction.ts @@ -174,12 +174,11 @@ export const advancedInteraction: TestInteractionData = { reason: "Check auction state parameters", assert: { type: AssertionInterfaceType.AUCTION, - isGraduated: false, + isGraduated: true, clearingPrice: "79228162514264337593543950336000", currencyRaised: "661363636363636363", latestCheckpoint: { clearingPrice: "79228162514264337593543950336000", - currencyRaisedQ96_X7: "523986256628430050902756580631272727272727272726000000", currencyRaisedAtClearingPriceQ96_X7: "0", cumulativeMpsPerPrice: "435754893828453856764491726848000", cumulativeMps: "5500000", diff --git a/test/e2e/instances/interaction/VariationInteraction.ts b/test/e2e/instances/interaction/VariationInteraction.ts index 26379276..6bb7f4b3 100644 --- a/test/e2e/instances/interaction/VariationInteraction.ts +++ b/test/e2e/instances/interaction/VariationInteraction.ts @@ -90,7 +90,7 @@ export const variationInteraction: TestInteractionData = { reason: "Check auction state with variance on currencyRaised", assert: { type: AssertionInterfaceType.AUCTION, - isGraduated: false, + isGraduated: true, clearingPrice: "79228162514264337593543950336000", // Currency raised will vary based on random bid amounts // Expected around 0.326 ETH with variance due to bid variations @@ -101,10 +101,6 @@ export const variationInteraction: TestInteractionData = { latestCheckpoint: { clearingPrice: "79228162514264337593543950336000", // These will also vary, so we use VariableAmount with raw amount variation - currencyRaisedQ96_X7: { - amount: "80593446990424197784923982579080313706988320995000000", - variation: "30%", // +/- 30% to account for variation - }, currencyRaisedAtClearingPriceQ96_X7: "0", cumulativeMpsPerPrice: { amount: "158456325028528675187087900672000", diff --git a/test/e2e/instances/setup/AdvancedSetup.ts b/test/e2e/instances/setup/AdvancedSetup.ts index 5485a842..71b61e1a 100644 --- a/test/e2e/instances/setup/AdvancedSetup.ts +++ b/test/e2e/instances/setup/AdvancedSetup.ts @@ -36,7 +36,6 @@ export const advancedSetup: TestSetupData = { }, ], }, - auctionParameters: { currency: "0x0000000000000000000000000000000000000000" as Address, auctionedToken: "AdvancedToken", @@ -47,8 +46,8 @@ export const advancedSetup: TestSetupData = { tickSpacing: "396140812571321687967719751680", validationHook: "0x0000000000000000000000000000000000000000" as Address, floorPrice: "79228162514264337593543950336000", + requiredCurrencyRaised: "0", }, - additionalTokens: [ { name: "AdvancedToken", diff --git a/test/e2e/instances/setup/ERC20Setup.ts b/test/e2e/instances/setup/ERC20Setup.ts index 6527e0a8..b3b15746 100644 --- a/test/e2e/instances/setup/ERC20Setup.ts +++ b/test/e2e/instances/setup/ERC20Setup.ts @@ -34,9 +34,10 @@ export const erc20Setup: TestSetupData = { fundsRecipient: "0x3333333333333333333333333333333333333333" as Address, auctionDurationBlocks: 50, claimDelayBlocks: 10, - tickSpacing: 100, + tickSpacing: 100n, validationHook: "0x0000000000000000000000000000000000000000" as Address, floorPrice: "79228162514264337593543950336000", + requiredCurrencyRaised: "0", }, additionalTokens: [ diff --git a/test/e2e/instances/setup/ExtendedSetup.ts b/test/e2e/instances/setup/ExtendedSetup.ts index 46055a81..1e37c3f0 100644 --- a/test/e2e/instances/setup/ExtendedSetup.ts +++ b/test/e2e/instances/setup/ExtendedSetup.ts @@ -100,6 +100,7 @@ export const extendedSetup: TestSetupData = { tickSpacing: "69324642199981300000000", // From params - large value as string validationHook: "0x0000000000000000000000000000000000000000" as Address, floorPrice: "6932464219998130000000000", // 0.0000875 ETH in Q96 format + requiredCurrencyRaised: "0", // Using the exact hex string from the params file auctionStepsData: "0x00000000000189c0325aa000000000010000000000001c2007a12000000000010000000000001c2007a12000000000010000000000001c200f424000000000010000000000001c200f424000000000010000000000001c200f424000000000010000000000001c202932e00000000001", @@ -112,11 +113,5 @@ export const extendedSetup: TestSetupData = { totalSupply: "1000000000000000000000000000", // 1 billion tokens (1e9 * 1e18) percentAuctioned: "15.0", // 15% of 1B = 150M tokens }, - { - name: "USDC", - decimals: "6", - totalSupply: "1000000000000", - percentAuctioned: "0.0", - }, ], }; diff --git a/test/e2e/instances/setup/SimpleSetup.ts b/test/e2e/instances/setup/SimpleSetup.ts index 77def4e5..5218230d 100644 --- a/test/e2e/instances/setup/SimpleSetup.ts +++ b/test/e2e/instances/setup/SimpleSetup.ts @@ -29,9 +29,10 @@ export const simpleSetup: TestSetupData = { fundsRecipient: "0x3333333333333333333333333333333333333333" as Address, auctionDurationBlocks: 50, claimDelayBlocks: 10, - tickSpacing: 100, + tickSpacing: 100n, validationHook: "0x0000000000000000000000000000000000000000" as Address, floorPrice: "79228162514264337593543950336000", + requiredCurrencyRaised: "0", }, additionalTokens: [ @@ -41,11 +42,5 @@ export const simpleSetup: TestSetupData = { totalSupply: "1000000000000000000000", percentAuctioned: "10.0", }, - { - name: "USDC", - decimals: "6", - totalSupply: "1000000000000", - percentAuctioned: "0.0", - }, ], }; diff --git a/test/e2e/instances/setup/VariationSetup.ts b/test/e2e/instances/setup/VariationSetup.ts index 24b85ca7..fc287691 100644 --- a/test/e2e/instances/setup/VariationSetup.ts +++ b/test/e2e/instances/setup/VariationSetup.ts @@ -34,9 +34,10 @@ export const variationSetup: TestSetupData = { fundsRecipient: "0x4444444444444444444444444444444444444444" as Address, auctionDurationBlocks: 50, claimDelayBlocks: 10, - tickSpacing: 100, + tickSpacing: 100n, validationHook: "0x0000000000000000000000000000000000000000" as Address, - floorPrice: "79228162514264337593543950336000", + floorPrice: 79228162514264337593543950336000n, + requiredCurrencyRaised: "0", }, additionalTokens: [ @@ -46,11 +47,5 @@ export const variationSetup: TestSetupData = { totalSupply: "1000000000000000000000", percentAuctioned: "10.0", }, - { - name: "USDC", - decimals: "6", - totalSupply: "1000000000000", - percentAuctioned: "0.0", - }, ], }; diff --git a/test/e2e/schemas/TestInteractionSchema.ts b/test/e2e/schemas/TestInteractionSchema.ts index 1e4febc8..f20acbce 100644 --- a/test/e2e/schemas/TestInteractionSchema.ts +++ b/test/e2e/schemas/TestInteractionSchema.ts @@ -133,7 +133,6 @@ export interface AuctionAssertion { export interface InternalCheckpointStruct { clearingPrice: string | VariableAmount; - currencyRaisedQ96_X7: string | VariableAmount; currencyRaisedAtClearingPriceQ96_X7: string | VariableAmount; cumulativeMpsPerPrice: string | VariableAmount; cumulativeMps: string | VariableAmount; diff --git a/test/e2e/schemas/TestSetupSchema.ts b/test/e2e/schemas/TestSetupSchema.ts index 868574cf..e2918705 100644 --- a/test/e2e/schemas/TestSetupSchema.ts +++ b/test/e2e/schemas/TestSetupSchema.ts @@ -47,10 +47,10 @@ export interface AuctionParameters { fundsRecipient: Address; auctionDurationBlocks: number; claimDelayBlocks: number; - tickSpacing: number | bigint | string; // number for small values, bigint/string for large values + tickSpacing: bigint | string; validationHook: Address; - floorPrice: string; - requiredCurrencyRaised?: string; // Optional: minimum currency that must be raised + floorPrice: bigint | string; + requiredCurrencyRaised: bigint | string; auctionStepsData?: string | StepData[]; // Optional: raw hex string or array of steps } diff --git a/test/e2e/src/AssertionEngine.ts b/test/e2e/src/AssertionEngine.ts index 426abb6a..a7a3b86f 100644 --- a/test/e2e/src/AssertionEngine.ts +++ b/test/e2e/src/AssertionEngine.ts @@ -7,11 +7,12 @@ import { Assertion, Address, VariableAmount, + InternalCheckpointStruct, } from "../schemas/TestInteractionSchema"; import { Contract } from "ethers"; import { TokenContract } from "./types"; import { AuctionDeployer } from "./AuctionDeployer"; -import { ZERO_ADDRESS, LOG_PREFIXES, ERROR_MESSAGES, TYPES, TYPE_FIELD } from "./constants"; +import { ZERO_ADDRESS, LOG_PREFIXES, ERROR_MESSAGES, TYPES, ONE_MILLION } from "./constants"; import { CheckpointStruct } from "../../../typechain-types/out/Auction"; import { resolveTokenAddress } from "./utils"; import hre from "hardhat"; @@ -58,11 +59,13 @@ export class AssertionEngine { if (assertion.type === AssertionInterfaceType.BALANCE) { await this.validateBalanceAssertion(assertion); } else if (assertion.type === AssertionInterfaceType.TOTAL_SUPPLY) { - await this.validateTotalSupplyAssertion([assertion]); + await this.validateTotalSupplyAssertions([assertion]); } else if (assertion.type === AssertionInterfaceType.EVENT) { - await this.validateEventAssertion([assertion]); + await this.validateEventAssertions([assertion]); } else if (assertion.type === AssertionInterfaceType.AUCTION) { - await this.validateAuctionAssertion([assertion]); + await this.validateAuctionAssertions([assertion]); + } else { + throw new Error(ERROR_MESSAGES.INVALID_ASSERTION_TYPE); } } @@ -88,12 +91,12 @@ export class AssertionEngine { // Percentage: convert to ratio and apply to expected (e.g., "5%" -> 5% of expected) const percentage = parseFloat(varianceStr.slice(0, -1)); const ratio = percentage / 100; - varianceAmount = (expected * BigInt(Math.floor(ratio * 1000000))) / 1000000n; + varianceAmount = (expected * BigInt(Math.floor(ratio * ONE_MILLION))) / BigInt(ONE_MILLION); } else { if (varianceStr.includes(".")) { const numericValue = parseFloat(varianceStr); // Ratio: treat as percentage in decimal form (e.g., "0.05" -> 5% of expected) - varianceAmount = (expected * BigInt(Math.floor(numericValue * 1000000))) / 1000000n; + varianceAmount = (expected * BigInt(Math.floor(numericValue * ONE_MILLION))) / BigInt(ONE_MILLION); } else { // Raw amount: use as absolute tolerance (e.g., "100000000000000000") varianceAmount = BigInt(varianceStr); @@ -175,7 +178,7 @@ export class AssertionEngine { * @param totalSupplyAssertion - Array of total supply assertions to validate * @throws Error if any total supply assertion fails or token is not found */ - async validateTotalSupplyAssertion(totalSupplyAssertion: TotalSupplyAssertion[]): Promise { + async validateTotalSupplyAssertions(totalSupplyAssertion: TotalSupplyAssertion[]): Promise { for (const assertion of totalSupplyAssertion) { const tokenAddress = await resolveTokenAddress(assertion.token, this.auctionDeployer); const token = await this.auctionDeployer.getTokenByAddress(tokenAddress); @@ -211,57 +214,12 @@ export class AssertionEngine { * @param auctionAssertions - Array of auction assertions, optionally including variances, to validate * @throws Error if any auction assertion fails */ - async validateAuctionAssertion(auctionAssertions: AuctionAssertion[]): Promise { + async validateAuctionAssertions(auctionAssertions: AuctionAssertion[]): Promise { for (const assertion of auctionAssertions) { console.log(LOG_PREFIXES.INFO, "Auction assertion validation"); - // Get the current auction state const auctionState = await this.getAuctionState(); - - for (const key of Object.keys(assertion)) { - if (key === TYPE_FIELD) continue; - if (key === "latestCheckpoint") continue; - let expected = assertion[key as keyof AuctionAssertion]; - if (expected != undefined && expected != null) { - if (!this.validateEquality(expected, auctionState[key as keyof AuctionState])) { - // Check if this is a VariableAmount with variance - const variance = typeof expected === "object" && "variation" in expected ? expected.variation : undefined; - throw new Error( - ERROR_MESSAGES.AUCTION_ASSERTION_FAILED( - typeof expected === "object" && "amount" in expected - ? expected.amount - : assertion[key as keyof AuctionAssertion], - auctionState[key as keyof AuctionState], - key, - variance, - ), - ); - } - } - } - - if (assertion.latestCheckpoint) { - for (const key of Object.keys(assertion.latestCheckpoint)) { - if (key === TYPE_FIELD) continue; - let expected = assertion.latestCheckpoint[key as keyof CheckpointStruct]; - if (expected != undefined && expected != null) { - if (!this.validateEquality(expected, auctionState.latestCheckpoint[key as keyof CheckpointStruct])) { - // Check if this is a VariableAmount with variance - const variance = typeof expected === "object" && "variation" in expected ? expected.variation : undefined; - throw new Error( - ERROR_MESSAGES.AUCTION_CHECKPOINT_ASSERTION_FAILED( - typeof expected === "object" && "amount" in expected - ? expected.amount - : assertion.latestCheckpoint[key as keyof CheckpointStruct], - auctionState.latestCheckpoint[key as keyof CheckpointStruct], - key, - variance, - ), - ); - } - } - } - } + this.validateAuctionAssertion(assertion, auctionState); const { type, ...assertionWithoutType } = assertion; console.log( LOG_PREFIXES.SUCCESS, @@ -273,12 +231,136 @@ export class AssertionEngine { } } + async validateAuctionAssertion(auctionAssertion: AuctionAssertion, auctionState: AuctionState): Promise { + if (auctionAssertion.isGraduated !== undefined) { + if (!this.validateEquality(auctionAssertion.isGraduated, auctionState.isGraduated)) { + this.throwAuctionAssertionError( + auctionAssertion.isGraduated, + auctionState.isGraduated, + "isGraduated", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (auctionAssertion.clearingPrice !== undefined) { + if (!this.validateEquality(auctionAssertion.clearingPrice, auctionState.clearingPrice)) { + this.throwAuctionAssertionError( + auctionAssertion.clearingPrice, + auctionState.clearingPrice, + "clearingPrice", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (auctionAssertion.currencyRaised !== undefined) { + if (!this.validateEquality(auctionAssertion.currencyRaised, auctionState.currencyRaised)) { + this.throwAuctionAssertionError( + auctionAssertion.currencyRaised, + auctionState.currencyRaised, + "currencyRaised", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (auctionAssertion.latestCheckpoint !== undefined) { + this.validateAuctionCheckpointAssertion(auctionAssertion.latestCheckpoint, auctionState.latestCheckpoint); + } + } + + async validateAuctionCheckpointAssertion( + checkpointAssertion: InternalCheckpointStruct, + stateCheckpoint: CheckpointStruct, + ): Promise { + if (checkpointAssertion.clearingPrice !== undefined) { + if (!this.validateEquality(checkpointAssertion.clearingPrice, stateCheckpoint.clearingPrice)) { + this.throwAuctionAssertionError( + checkpointAssertion.clearingPrice, + stateCheckpoint.clearingPrice, + "clearingPrice", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (checkpointAssertion.currencyRaisedAtClearingPriceQ96_X7 !== undefined) { + if ( + !this.validateEquality( + checkpointAssertion.currencyRaisedAtClearingPriceQ96_X7, + stateCheckpoint.currencyRaisedAtClearingPriceQ96_X7, + ) + ) { + this.throwAuctionAssertionError( + checkpointAssertion.currencyRaisedAtClearingPriceQ96_X7, + stateCheckpoint.currencyRaisedAtClearingPriceQ96_X7, + "currencyRaisedAtClearingPriceQ96_X7", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (checkpointAssertion.cumulativeMpsPerPrice !== undefined) { + if (!this.validateEquality(checkpointAssertion.cumulativeMpsPerPrice, stateCheckpoint.cumulativeMpsPerPrice)) { + this.throwAuctionAssertionError( + checkpointAssertion.cumulativeMpsPerPrice, + stateCheckpoint.cumulativeMpsPerPrice, + "cumulativeMpsPerPrice", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (checkpointAssertion.cumulativeMps !== undefined) { + if (!this.validateEquality(checkpointAssertion.cumulativeMps, stateCheckpoint.cumulativeMps)) { + this.throwAuctionAssertionError( + checkpointAssertion.cumulativeMps, + stateCheckpoint.cumulativeMps, + "cumulativeMps", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (checkpointAssertion.prev !== undefined) { + if (!this.validateEquality(checkpointAssertion.prev, stateCheckpoint.prev)) { + this.throwAuctionAssertionError( + checkpointAssertion.prev, + stateCheckpoint.prev, + "prev", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + if (checkpointAssertion.next !== undefined) { + if (!this.validateEquality(checkpointAssertion.next, stateCheckpoint.next)) { + this.throwAuctionAssertionError( + checkpointAssertion.next, + stateCheckpoint.next, + "next", + ERROR_MESSAGES.AUCTION_ASSERTION_FAILED, + ); + } + } + } + + async throwAuctionAssertionError( + expected: any, + actual: any, + field: string, + errorFunction: (expected: any, actual: any, field?: string, variance?: string) => string, + ): Promise { + const variance = typeof expected === "object" && "variation" in expected ? expected.variation : undefined; + throw new Error( + errorFunction( + typeof expected === "object" && "amount" in expected ? expected.amount : expected, + actual, + field, + variance, + ), + ); + } + /** * Validates event assertions by checking if specific events were emitted in the current block. * @param eventAssertion - Array of event assertions to validate * @throws Error if any event assertion fails or block is not found */ - async validateEventAssertion(eventAssertion: EventAssertion[]): Promise { + async validateEventAssertions(eventAssertion: EventAssertion[]): Promise { for (const assertion of eventAssertion) { console.log(LOG_PREFIXES.INFO, "Event assertion validation for event:", assertion.eventName); @@ -355,13 +437,7 @@ export class AssertionEngine { return true; // No args to check, just event name match is enough } for (const [, expectedValue] of Object.entries(expectedArgs)) { - let contains = false; - softMatchLoop: for (let i = 0; i < actualArgs.length; i++) { - if (actualArgs[i].toString() == expectedValue.toString()) { - contains = true; - break softMatchLoop; - } - } + let contains = actualArgs.some((value: any) => value == expectedValue).length > 0; if (!contains) { return false; } @@ -397,6 +473,12 @@ export class AssertionEngine { * @returns BidderState object containing address, tokenBalance, and currencyBalance */ async getBidderState(bidderAddress: Address): Promise { + if (this.token == undefined) { + throw new Error(ERROR_MESSAGES.TOKEN_UNSET); + } + if (this.currency === undefined) { + throw new Error(ERROR_MESSAGES.CURRENCY_UNSET); + } const tokenBalance = this.token ? await this.token.balanceOf(bidderAddress) : 0n; const currencyBalance = this.currency ? await this.currency.balanceOf(bidderAddress) : 0n; diff --git a/test/e2e/src/AuctionDeployer.ts b/test/e2e/src/AuctionDeployer.ts index d2749fbb..114712e1 100644 --- a/test/e2e/src/AuctionDeployer.ts +++ b/test/e2e/src/AuctionDeployer.ts @@ -316,7 +316,11 @@ export class AuctionDeployer { ? BigInt(auctionParameters.tickSpacing) : auctionParameters.tickSpacing, validationHook: auctionParameters.validationHook, - floorPrice: auctionParameters.floorPrice, + floorPrice: + typeof auctionParameters.floorPrice === "string" + ? BigInt(auctionParameters.floorPrice) + : auctionParameters.floorPrice, + requiredCurrencyRaised: auctionParameters.requiredCurrencyRaised, auctionStepsData: auctionStepsData, }; } @@ -381,8 +385,8 @@ export class AuctionDeployer { claimBlock: config.claimBlock, tickSpacing: config.tickSpacing, validationHook: config.validationHook, - floorPrice: config.floorPrice, - requiredCurrencyRaised: config.requiredCurrencyRaised || 0, + floorPrice: typeof config.floorPrice === "string" ? BigInt(config.floorPrice) : config.floorPrice, + requiredCurrencyRaised: config.requiredCurrencyRaised, auctionStepsData: config.auctionStepsData, }; diff --git a/test/e2e/src/BidSimulator.ts b/test/e2e/src/BidSimulator.ts index 91529f57..cc77c54a 100644 --- a/test/e2e/src/BidSimulator.ts +++ b/test/e2e/src/BidSimulator.ts @@ -8,6 +8,7 @@ import { UINT_48_MAX, LOG_PREFIXES, ERROR_MESSAGES, + ONE_MILLION, } from "./constants"; import { IAllowanceTransfer } from "../../../typechain-types/test/e2e/artifacts/permit2/src/interfaces/IAllowanceTransfer"; import { TransactionInfo } from "./types"; @@ -115,8 +116,8 @@ export class BidSimulator { const factor = Math.pow(recurringBid.amountFactor, i); // Use BigInt arithmetic to avoid scientific notation conversion const originalValue = BigInt(recurringBid.amount.value.toString()); - const factorScaled = Math.floor(factor * 1000000); // Scale factor to avoid decimals - const adjustedValue = (originalValue * BigInt(factorScaled)) / BigInt(1000000); + const factorScaled = Math.floor(factor * ONE_MILLION); // Scale factor to avoid decimals + const adjustedValue = (originalValue * BigInt(factorScaled)) / BigInt(ONE_MILLION); adjustedAmount = { ...recurringBid.amount, value: adjustedValue.toString(), @@ -127,8 +128,8 @@ export class BidSimulator { const factor = recurringBid.priceFactor * i; // Use BigInt arithmetic to avoid scientific notation conversion const originalValue = BigInt(recurringBid.price.value.toString()); - const factorScaled = Math.floor(factor * 1000000); // Scale factor to avoid decimals - const adjustedValue = (originalValue * BigInt(factorScaled)) / BigInt(1000000); + const factorScaled = Math.floor(factor * ONE_MILLION); // Scale factor to avoid decimals + const adjustedValue = (originalValue * BigInt(factorScaled)) / BigInt(ONE_MILLION); adjustedPrice = { ...recurringBid.price, value: adjustedValue.toString(), @@ -442,6 +443,8 @@ export class BidSimulator { let tx = await this.auction.getFunction("sweepUnsoldTokens").populateTransaction(); let msg = ` Sweeping unsold tokens`; transactionInfos.push({ tx, from: null, msg }); + } else { + throw new Error(ERROR_MESSAGES.INVALID_ACTION_METHOD); } } } diff --git a/test/e2e/src/SingleTestRunner.ts b/test/e2e/src/SingleTestRunner.ts index a63db780..7bb57501 100644 --- a/test/e2e/src/SingleTestRunner.ts +++ b/test/e2e/src/SingleTestRunner.ts @@ -166,15 +166,14 @@ export class SingleTestRunner { // Don't add final state as a checkpoint - only use checkpoints from actual events // The contract only validates hints against stored checkpoints from CheckpointUpdated events let calculatedCurrencyRaised = -1; - // Exit and claim all bids if auction graduated - if (finalState.isGraduated) { - calculatedCurrencyRaised = await this.exitAndClaimAllBids( - auction, - setupData, - currencyToken, - bidSimulator.getReverseLabelMap(), - ); - } + // Exit and claim all bids + calculatedCurrencyRaised = await this.exitAndClaimAllBids( + auction, + setupData, + currencyToken, + bidSimulator.getReverseLabelMap(), + finalState.isGraduated, + ); // Log balances for all funded accounts (after claiming) await this.logAccountBalances(setupData, this.deployer, auctionedToken); @@ -413,6 +412,7 @@ export class SingleTestRunner { setupData: TestSetupData, currencyToken: TokenContract | null, reverseLabelMap: Map, + isGraduated: boolean, ): Promise { let calculatedCurrencyRaised = 0; console.log("\n๐ŸŽซ Exiting and claiming all bids..."); @@ -519,6 +519,10 @@ export class SingleTestRunner { calculatedCurrencyRaised += Number(bid.amount - refundAmount) / 10 ** currencyDecimals; } + if (!isGraduated) { + continue; // Skip claiming if auction didn't graduate + } + // Claim all tokens in batch const unfilteredBidIds = bids.map((b) => b.bidId); const bidIds = unfilteredBidIds.filter((bidId) => shouldClaimBid.get(bidId)); @@ -703,7 +707,7 @@ export class SingleTestRunner { createAuctionBlock: string, offsetBlocks: number, ): Promise { - // Collect all events (bids, actions, assertions) and sort by block + // Collect all objectives (bids, actions, assertions) and sort by block const allEvents: EventData[] = []; // Add bids using BidSimulator's collection logic diff --git a/test/e2e/src/constants.ts b/test/e2e/src/constants.ts index 0ec700e6..edf1c59f 100644 --- a/test/e2e/src/constants.ts +++ b/test/e2e/src/constants.ts @@ -23,7 +23,8 @@ export const HARDHAT_METHODS = { } as const; // True constants - values that never change -export const MPS = 10000000; // 1e7 - Million Price Steps +export const MPS = 10000000; // 1e7 - mBPS +export const ONE_MILLION = 1000000; export const MAX_SYMBOL_LENGTH = 4; export const HEX_PADDING_LENGTH = 16; export const DEFAULT_TOTAL_SUPPLY = "1000000000000000000000"; // 1000 tokens with 18 decimals @@ -62,6 +63,9 @@ export const ERROR_MESSAGES = { variance ? `Balance assertion failed for ${address} token ${token}. Expected ${expected} ยฑ ${variance}, got ${actual}` : `Balance assertion failed for ${address} token ${token}. Expected ${expected}, got ${actual}`, + INVALID_ASSERTION_TYPE: `Invalid assertion type`, + TOKEN_UNSET: `Token is not set`, + CURRENCY_UNSET: `Currency is not set`, // BidSimulator errors PERCENT_OF_SUPPLY_INVALID_SIDE: @@ -69,6 +73,7 @@ export const ERROR_MESSAGES = { EXPECTED_REVERT_NOT_FOUND: (expected: string, actual: string) => `Expected revert data to contain "${expected}", but got: ${actual}`, PREVIOUS_TICK_OR_TICK_PRICE_NOT_PROVIDED: "previousTick or prevTickPrice must be provided", + INVALID_ACTION_METHOD: "Invalid action method", // E2ECliRunner errors NO_INSTANCE_FOUND: (filePath: string) => `No instance found in ${filePath}`, diff --git a/test/e2e/src/types.ts b/test/e2e/src/types.ts index 7f945665..56b8443b 100644 --- a/test/e2e/src/types.ts +++ b/test/e2e/src/types.ts @@ -93,10 +93,10 @@ export interface AuctionConfig { startBlock: number; endBlock: number; claimBlock: number; - tickSpacing: number | bigint | string; // number for small values, bigint/string for large values + tickSpacing: bigint | string; validationHook: Address; - floorPrice: string; - requiredCurrencyRaised?: string; // Optional: minimum currency that must be raised + floorPrice: bigint | string; + requiredCurrencyRaised: bigint | string; auctionStepsData: string; } diff --git a/test/e2e/src/utils.ts b/test/e2e/src/utils.ts index ec162133..39993e5d 100644 --- a/test/e2e/src/utils.ts +++ b/test/e2e/src/utils.ts @@ -28,10 +28,16 @@ export async function resolveTokenAddress(tokenIdentifier: string, auctionDeploy * @param tickSpacing - The auction's tick spacing * @returns The price in X96 format as a bigint */ -export function tickNumberToPriceX96(tickNumber: number, floorPrice: bigint, tickSpacing: bigint): bigint { +export function tickNumberToPriceX96( + tickNumber: number, + floorPrice: bigint | string, + tickSpacing: bigint | string, +): bigint { // Floor price is already in Q96, tick spacing is raw // price = floorPrice + (tickNumber - 1) * tickSpacing - return floorPrice + (BigInt(tickNumber) - 1n) * tickSpacing; + const floorPriceBigInt = typeof floorPrice === "string" ? BigInt(floorPrice) : floorPrice; + const tickSpacingBigInt = typeof tickSpacing === "string" ? BigInt(tickSpacing) : tickSpacing; + return floorPriceBigInt + (BigInt(tickNumber) - 1n) * tickSpacingBigInt; } /** @@ -42,8 +48,8 @@ export function tickNumberToPriceX96(tickNumber: number, floorPrice: bigint, tic */ export async function calculatePrice( priceConfig: PriceConfig, - floorPrice?: bigint, - tickSpacing?: bigint, + floorPrice?: bigint | string, + tickSpacing?: bigint | string, ): Promise { let value: bigint; From 9d91aa1aabd3dfd623124a2e7eeccf8eea2781a8 Mon Sep 17 00:00:00 2001 From: Eric Sanchirico Date: Thu, 23 Oct 2025 20:00:48 -0400 Subject: [PATCH 4/5] fix currencyRaised calculation --- test/e2e/src/SingleTestRunner.ts | 104 +++++++++++++++++-------------- test/e2e/src/constants.ts | 1 + 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/test/e2e/src/SingleTestRunner.ts b/test/e2e/src/SingleTestRunner.ts index 7bb57501..44df395b 100644 --- a/test/e2e/src/SingleTestRunner.ts +++ b/test/e2e/src/SingleTestRunner.ts @@ -93,6 +93,7 @@ export class SingleTestRunner { // PHASE 1: Setup the auction environment console.log(LOG_PREFIXES.INFO, "Phase 1: Setting up auction environment..."); + await hre.network.provider.send(METHODS.EVM.SET_AUTOMINE, [false]); // Check current block and auction start block const currentBlock = await hre.ethers.provider.getBlockNumber(); @@ -159,8 +160,11 @@ export class SingleTestRunner { const blocksToMine = auctionEndBlock - blockBeforeFinalState + 1; await hre.ethers.provider.send("hardhat_mine", [`0x${blocksToMine.toString(16)}`]); } + await hre.network.provider.send(METHODS.EVM.SET_AUTOMINE, [true]); - // Get final state as-is (without forcing a checkpoint) + // Force checkpoint and call sweep functions + await this.forceCheckpointAndSweep(auction); + // Get final state as-is const finalState = await assertionEngine.getAuctionState(); // Don't add final state as a checkpoint - only use checkpoints from actual events @@ -191,6 +195,18 @@ export class SingleTestRunner { }; } + private async forceCheckpointAndSweep(auction: Contract): Promise { + // Force checkpoint + await auction.checkpoint(); + try { + await auction.sweepCurrency(); + } catch (error) { + console.log(LOG_PREFIXES.INFO, ERROR_MESSAGES.CURRENCY_SWEEP_ATTEMPT_FAILED, error); + } + + await auction.sweepUnsoldTokens(); + } + private async logSummary( auction: Contract, currencyToken: TokenContract | null, @@ -466,48 +482,51 @@ export class SingleTestRunner { } const bidId = bid.bidId; const maxPrice = bid.maxPrice; + let skipPartial = false; // Check if bid is above, at, or below final clearing price if (maxPrice > finalCheckpoint.clearingPrice) { // Bid is above final clearing - try simple exitBid first try { - let tx = await auction.exitBid(bidId); const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; console.log(` โœ… Exited bid ${bidId} at block ${previousBlock} (simple exit - above clearing)`); shouldClaimBid.set(bidId, true); - continue; // Successfully exited, move to next bid + skipPartial = true; } catch (error) { // Simple exit failed, fall through to try partial exit } } - - // Try partial exit (for bids at/below clearing, or if simple exit failed) - try { - const hints = this.findCheckpointHints(maxPrice, sortedCheckpoints); - if (hints) { - let tx = await auction.exitPartiallyFilledBid(bidId, hints.lastFullyFilled, hints.outbid); - const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; - console.log( - ` โœ… Exited bid ${bidId} at block ${previousBlock} (partial exit with hints: ${hints.lastFullyFilled}, ${hints.outbid})`, - ); - const receipt = await hre.ethers.provider.getTransactionReceipt(tx.hash); - for (const log of receipt?.logs ?? []) { - const parsedLog = auction.interface.parseLog({ - topics: log.topics, - data: log.data, - }); - if (parsedLog?.name === EVENTS.BID_EXITED) { - if (parsedLog.args[2] > 0n) { - shouldClaimBid.set(bidId, true); + if (!skipPartial) { + // Try partial exit (for bids at/below clearing, or if simple exit failed) + try { + const hints = this.findCheckpointHints(maxPrice, sortedCheckpoints); + if (hints) { + let tx = await auction.exitPartiallyFilledBid(bidId, hints.lastFullyFilled, hints.outbid); + const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; + console.log( + ` โœ… Exited bid ${bidId} at block ${previousBlock} (partial exit with hints: ${hints.lastFullyFilled}, ${hints.outbid})`, + ); + const receipt = await hre.ethers.provider.getTransactionReceipt(tx.hash); + for (const log of receipt?.logs ?? []) { + const parsedLog = auction.interface.parseLog({ + topics: log.topics, + data: log.data, + }); + if (parsedLog?.name === EVENTS.BID_EXITED) { + if (parsedLog.args[2] > 0n) { + shouldClaimBid.set(bidId, true); + } } } + } else { + console.log(` โš ๏ธ Could not find checkpoint hints for bid ${bidId} (price: ${maxPrice})`); } - } else { - console.log(` โš ๏ธ Could not find checkpoint hints for bid ${bidId} (price: ${maxPrice})`); + } catch (partialExitError) { + const errorMsg = partialExitError instanceof Error ? partialExitError.message : String(partialExitError); + const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; + console.log( + ` โš ๏ธ Could not exit bid ${bidId} at block ${previousBlock}: ${errorMsg.substring(0, 200)}`, + ); } - } catch (partialExitError) { - const errorMsg = partialExitError instanceof Error ? partialExitError.message : String(partialExitError); - const previousBlock = (await hre.ethers.provider.getBlockNumber()) - 1; - console.log(` โš ๏ธ Could not exit bid ${bidId} at block ${previousBlock}: ${errorMsg.substring(0, 200)}`); } let balanceAfter = 0n; if (currencyToken) { @@ -517,6 +536,9 @@ export class SingleTestRunner { } const refundAmount = balanceAfter - balanceBefore; calculatedCurrencyRaised += Number(bid.amount - refundAmount) / 10 ** currencyDecimals; + if (bid.amount == 0n) { + throw Error("bid 0"); + } } if (!isGraduated) { @@ -643,8 +665,6 @@ export class SingleTestRunner { async handleInitializeTransactions(setupTransactions: TransactionInfo[]): Promise { const provider = hre.ethers.provider; - // Pause automine so all txs pile up - await provider.send(METHODS.EVM.SET_AUTOMINE, [false]); await provider.send(METHODS.EVM.SET_INTERVAL_MINING, [0]); const nextNonce = new Map(); @@ -684,8 +704,6 @@ export class SingleTestRunner { // mine exactly one block await provider.send(METHODS.EVM.MINE, []); - - await provider.send(METHODS.EVM.SET_AUTOMINE, [true]); } /** @@ -857,29 +875,21 @@ export class SingleTestRunner { */ async handleTransactions(transactions: TransactionInfo[], reverseLabelMap: Map): Promise { const provider = hre.network.provider; - - // Pause automining so txs pile up in the mempool - await provider.send(METHODS.EVM.SET_AUTOMINE, [false]); await provider.send(METHODS.EVM.SET_INTERVAL_MINING, [0]); // ensure no timer mines a block const pendingHashes: HashWithRevert[] = []; const nextNonce = new Map(); - try { - await this.submitTransactions(transactions, pendingHashes, nextNonce, reverseLabelMap, provider); + await this.submitTransactions(transactions, pendingHashes, nextNonce, reverseLabelMap, provider); - // Mine exactly one block containing all pending txs - await provider.send(METHODS.EVM.MINE, []); + // Mine exactly one block containing all pending txs + await provider.send(METHODS.EVM.MINE, []); - const receipts = await this.validateReceipts(pendingHashes); + const receipts = await this.validateReceipts(pendingHashes); - // Track bidIds from BidSubmitted events - if (this.auction && receipts.length > 0) { - await this.trackBidIdsFromReceipts(receipts, this.auction); - } - } finally { - // Restore automining - await provider.send(METHODS.EVM.SET_AUTOMINE, [true]); + // Track bidIds from BidSubmitted events + if (this.auction && receipts.length > 0) { + await this.trackBidIdsFromReceipts(receipts, this.auction); } } diff --git a/test/e2e/src/constants.ts b/test/e2e/src/constants.ts index edf1c59f..c97a99a8 100644 --- a/test/e2e/src/constants.ts +++ b/test/e2e/src/constants.ts @@ -84,6 +84,7 @@ export const ERROR_MESSAGES = { `Current block ${currentBlock} is already past auction start block ${auctionStartBlock}`, BLOCK_ALREADY_MINED: (blockNum: number) => `Block ${blockNum} is already mined`, EXPECTED_REVERT_MISMATCH: (expected: string, actual: string) => `Expected revert "${expected}" but got: ${actual}`, + CURRENCY_SWEEP_ATTEMPT_FAILED: "Currency sweep attempt failed", // SchemaValidator errors TYPESCRIPT_FILE_NOT_FOUND: (tsFilePath: string) => `TypeScript test instance file not found: ${tsFilePath}`, From eb41082475cb246eaff300d47d1093e15be7d6c1 Mon Sep 17 00:00:00 2001 From: Eric Sanchirico Date: Thu, 23 Oct 2025 20:12:11 -0400 Subject: [PATCH 5/5] force checkpoint and sweeps --- test/e2e/src/SingleTestRunner.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/e2e/src/SingleTestRunner.ts b/test/e2e/src/SingleTestRunner.ts index 44df395b..1207be62 100644 --- a/test/e2e/src/SingleTestRunner.ts +++ b/test/e2e/src/SingleTestRunner.ts @@ -167,8 +167,6 @@ export class SingleTestRunner { // Get final state as-is const finalState = await assertionEngine.getAuctionState(); - // Don't add final state as a checkpoint - only use checkpoints from actual events - // The contract only validates hints against stored checkpoints from CheckpointUpdated events let calculatedCurrencyRaised = -1; // Exit and claim all bids calculatedCurrencyRaised = await this.exitAndClaimAllBids(