├── .env ├── .eslintignore ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── advanced.md ├── eslintrc.js ├── funding.json ├── hardhat.config.js ├── package.json ├── scripts ├── gen-options-md.ts └── run-tests.sh ├── src ├── constants.ts ├── index.ts ├── lib │ ├── artifacts.ts │ ├── collector.ts │ ├── gasData.ts │ ├── options.ts │ ├── provider.ts │ ├── render │ │ ├── index.ts │ │ ├── json.ts │ │ ├── legacy.ts │ │ ├── markdown.ts │ │ └── terminal.ts │ ├── resolvers │ │ ├── etherrouter.ts │ │ ├── index.ts │ │ └── oz.ts │ └── validators │ │ ├── foundry.ts │ │ └── hardhat.ts ├── task-names.ts ├── tasks │ └── mergeReports.ts ├── type-extensions.ts ├── types.ts └── utils │ ├── chains.ts │ ├── gas.ts │ ├── prices.ts │ ├── sources.ts │ └── ui.ts ├── test ├── helpers.ts ├── integration │ ├── forked.ts │ ├── node.ts │ ├── options.a.ts │ ├── options.b.ts │ ├── options.c.ts │ ├── options.default.ts │ ├── options.e.ts │ ├── options.f.ts │ ├── options.g.ts │ ├── oz.ts │ ├── parallel.ts │ ├── run.ts │ └── viem.ts ├── projects │ ├── forked │ │ ├── abi.ts │ │ ├── contracts │ │ │ ├── ContractA.sol │ │ │ └── WETH.sol │ │ ├── hardhat.config.ts │ │ └── test │ │ │ ├── reset.ts │ │ │ └── weth.ts │ ├── merge │ │ ├── .gitignore │ │ ├── hardhat.config.ts │ │ ├── malformatted-0.json │ │ ├── malformatted-1.json │ │ ├── mergeOutput-0.json │ │ ├── mergeOutput-1.json │ │ └── mergeOutput.expected.json │ ├── options │ │ ├── contracts │ │ │ ├── DuplicateA.sol │ │ │ ├── DuplicateB.sol │ │ │ ├── EtherRouter │ │ │ │ ├── EtherRouter.sol │ │ │ │ ├── Factory.sol │ │ │ │ ├── Resolver.sol │ │ │ │ ├── VersionA.sol │ │ │ │ └── VersionB.sol │ │ │ ├── Immutable.sol │ │ │ ├── MultiContractFile.sol │ │ │ ├── Undeployed.sol │ │ │ ├── VariableConstructor.sol │ │ │ ├── VariableCosts.sol │ │ │ └── Wallets │ │ │ │ └── Wallet.sol │ │ ├── hardhat.default.config.ts │ │ ├── hardhat.options.a.config.ts │ │ ├── hardhat.options.b.config.ts │ │ ├── hardhat.options.c.config.ts │ │ ├── hardhat.options.e.config.ts │ │ ├── hardhat.options.f.config.ts │ │ ├── hardhat.options.g.config.ts │ │ └── test │ │ │ ├── duplicatenames.ts │ │ │ ├── etherrouter.ts │ │ │ ├── factory.ts │ │ │ ├── immutable.ts │ │ │ ├── multicontract.ts │ │ │ ├── variableconstructor.ts │ │ │ ├── variablecosts.ts │ │ │ └── wallet.ts │ ├── oz │ │ ├── contracts │ │ │ ├── BeaconBox.sol │ │ │ ├── BeaconBoxV2.sol │ │ │ ├── ProxyBox.sol │ │ │ └── ProxyBoxV2.sol │ │ ├── hardhat.config.ts │ │ └── test │ │ │ └── box.ts │ ├── run │ │ ├── contracts │ │ │ ├── Base.sol │ │ │ ├── Flashswap.sol │ │ │ └── KyberNetworkProxy.sol │ │ ├── hardhat.config.ts │ │ └── script.ts │ └── viem │ │ ├── contracts │ │ ├── EmphaticGreeter.sol │ │ ├── Greeter.sol │ │ └── UrGreeter.sol │ │ ├── hardhat.config.ts │ │ └── test │ │ └── greeter.ts ├── tasks │ └── merge.ts └── unit │ ├── cases │ ├── arbitrum.ts │ ├── base.ts │ ├── evm.ts │ └── optimism.ts │ ├── gas.ts │ └── prices.ts ├── tsconfig.json ├── tsconfig.prod.json ├── tslint.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | TS_NODE_FILES=true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/projects/viem/artifacts/** 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: 8 | - "**" 9 | workflow_dispatch: 10 | 11 | env: 12 | CMC_API_KEY: ${{ secrets.CMC_API_KEY }} 13 | POLYGONSCAN_API_KEY: ${{ secrets.POLYGONSCAN_API_KEY }} 14 | ALCHEMY_TOKEN: ${{ secrets.ALCHEMY_TOKEN }} 15 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} 16 | OPTIMISTIC_API_KEY: ${{ secrets.OPTIMISTIC_API_KEY }} 17 | BASE_API_KEY: ${{ secrets.BASE_API_KEY }} 18 | ARBITRUM_API_KEY: ${{ secrets.ARBITRUM_API_KEY }} 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 22 28 | cache: 'yarn' 29 | - run: yarn 30 | - run: yarn lint 31 | 32 | build: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: 22 39 | cache: 'yarn' 40 | - run: yarn 41 | - run: yarn build 42 | 43 | test: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | - uses: actions/setup-node@v3 48 | with: 49 | node-version: 22 50 | cache: 'yarn' 51 | - run: yarn 52 | - run: yarn test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # OSX 24 | .DS_STORE 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | 66 | # hardhat-project 67 | test/projects/options/artifacts 68 | test/projects/options/cache 69 | test/projects/options/gasReporterOutput.json 70 | test/projects/options/gas.json 71 | test/projects/options/testGasReport.txt 72 | 73 | test/projects/waffle/artifacts 74 | test/projects/waffle/cache 75 | test/projects/waffle/gasReporterOutput.json 76 | 77 | test/projects/forked/artifacts 78 | test/projects/forked/cache 79 | test/projects/forked/gasReporterOutput.json 80 | 81 | test/projects/viem/artifacts 82 | test/projects/viem/cache 83 | test/projects/viem/gasReporterOutput.json 84 | 85 | test/projects/oz/artifacts 86 | test/projects/oz/cache 87 | test/projects/oz/gasReporterOutput.json 88 | 89 | test/projects/run/artifacts 90 | test/projects/run/cache 91 | test/projects/run/gasReporterOutput.json 92 | 93 | dist/ 94 | 95 | options.txt 96 | 97 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require" : [ 3 | "dotenv/config", 4 | "ts-node/register", 5 | "source-map-support/register" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.9 / 2022-09-05 2 | * Import libraries when they are needed (https://github.com/cgewecke/hardhat-gas-reporter/issues/126) 3 | 4 | # 1.0.8 / 2022-02-15 5 | * Skip gas reporting when --parallel flag detected (https://github.com/cgewecke/hardhat-gas-reporter/issues/101) 6 | 7 | # 1.0.7 / 2022-01-09 8 | * Pin colors.js to 1.4.0 9 | 10 | # 1.0.6 / 2021-11-29 11 | * Add missing config TS types for multi chain gas prices (https://github.com/cgewecke/hardhat-gas-reporter/issues/81) 12 | 13 | # 1.0.5 / 2021-11-29 14 | * Fix remote data fetching race condition (https://github.com/cgewecke/hardhat-gas-reporter/issues/80) 15 | * Update eth-gas-reporter to 0.2.23 (https://github.com/cgewecke/hardhat-gas-reporter/issues/80) 16 | * Add gas reporter merge task (https://github.com/cgewecke/hardhat-gas-reporter/issues/75) 17 | * Bump y18n from 3.2.1 to 3.2.2 (https://github.com/cgewecke/hardhat-gas-reporter/issues/61) 18 | * Bump hosted-git-info from 2.8.8 to 2.8.9 (https://github.com/cgewecke/hardhat-gas-reporter/issues/64) 19 | * Bump normalize-url from 4.5.0 to 4.5.1 (https://github.com/cgewecke/hardhat-gas-reporter/issues/66) 20 | * Bump glob-parent from 5.1.1 to 5.1.2 (https://github.com/cgewecke/hardhat-gas-reporter/issues/67) 21 | * Bump tar from 4.4.13 to 4.4.15 (https://github.com/cgewecke/hardhat-gas-reporter/issues/70) 22 | * Bump path-parse from 1.0.6 to 1.0.7 (https://github.com/cgewecke/hardhat-gas-reporter/issues/71) 23 | dependabot 24 | 25 | # 1.0.4 / 2020-12-21 26 | * Fix bug that caused txs signed with Waffle wallets to be omitted from report (https://github.com/cgewecke/hardhat-gas-reporter/issues/54) 27 | 28 | # 1.0.3 / 2020-12-02 29 | * Fix excludeContracts option & support folders (https://github.com/cgewecke/hardhat-gas-reporter/issues/51) 30 | 31 | # 1.0.2 / 2020-12-01 32 | * Add remoteContracts option (https://github.com/cgewecke/hardhat-gas-reporter/issues/49) 33 | * Fix null receipt crash (https://github.com/cgewecke/hardhat-gas-reporter/issues/48) 34 | 35 | # 1.0.1 / 2020-11-09 36 | * Make all EthGasReporterConfig types optional (https://github.com/cgewecke/hardhat-gas-reporter/issues/45) 37 | * Fix test return value bug by returning result of runSuper (https://github.com/cgewecke/hardhat-gas-reporter/issues/44) 38 | 39 | # 1.0.0 / 2020-10-29 40 | * Refactor as hardhat-gas-reporter (https://github.com/cgewecke/hardhat-gas-reporter/issues/38) 41 | * Update eth-gas-reporter to 0.2.19 42 | 43 | # 0.1.4 / 2020-10-13 44 | * Add coinmarketcap to types 45 | * Support in-process BuidlerEVM / remove "independent node" requirement 46 | 47 | # 0.1.3 / 2019-12-01 48 | * Upgrade artifacts handler for Buidler 1.x, fixing missing deployment data 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nomic Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es6: true, 5 | node: true, 6 | }, 7 | parser: "@typescript-eslint/parser", 8 | parserOptions: { 9 | project: "./tsconfig.json", 10 | sourceType: "module", 11 | }, 12 | plugins: [ 13 | "import", 14 | "no-only-tests", 15 | "@typescript-eslint", 16 | ], 17 | rules: { 18 | "@typescript-eslint/adjacent-overload-signatures": "error", 19 | "@typescript-eslint/array-type": [ 20 | "error", 21 | { 22 | default: "array-simple", 23 | }, 24 | ], 25 | "@typescript-eslint/await-thenable": "error", 26 | "@typescript-eslint/consistent-type-assertions": "error", 27 | "@typescript-eslint/consistent-type-definitions": "error", 28 | "@typescript-eslint/dot-notation": "error", 29 | "@typescript-eslint/explicit-member-accessibility": [ 30 | "error", 31 | { 32 | accessibility: "explicit", 33 | overrides: { 34 | constructors: "no-public", 35 | }, 36 | }, 37 | ], 38 | "@typescript-eslint/naming-convention": [ 39 | "error", 40 | { 41 | selector: "default", 42 | format: ["camelCase"], 43 | leadingUnderscore: "allow", 44 | trailingUnderscore: "allow", 45 | }, 46 | { 47 | selector: ["variable", "parameter"], 48 | format: ["camelCase", "UPPER_CASE", "PascalCase"], 49 | leadingUnderscore: "allow", 50 | trailingUnderscore: "allow", 51 | }, 52 | { 53 | selector: ["classProperty"], 54 | format: ["camelCase", "UPPER_CASE"], 55 | leadingUnderscore: "allow", 56 | }, 57 | { 58 | selector: ["classProperty"], 59 | modifiers: ["private"], 60 | format: ["camelCase", "UPPER_CASE"], 61 | leadingUnderscore: "require", 62 | }, 63 | { 64 | selector: "enumMember", 65 | format: ["UPPER_CASE"], 66 | }, 67 | { 68 | selector: "memberLike", 69 | modifiers: ["private"], 70 | format: ["camelCase"], 71 | leadingUnderscore: "require", 72 | }, 73 | { 74 | selector: ["objectLiteralProperty"], 75 | format: null, 76 | }, 77 | { 78 | selector: ["objectLiteralMethod"], 79 | format: ["camelCase", "PascalCase", "snake_case", "UPPER_CASE"], 80 | leadingUnderscore: "allow", 81 | }, 82 | { 83 | selector: "typeProperty", 84 | format: ["camelCase", "PascalCase"], 85 | leadingUnderscore: "allow", 86 | }, 87 | { 88 | selector: "typeLike", 89 | format: ["PascalCase"], 90 | }, 91 | { 92 | selector: "typeProperty", 93 | filter: "__hardhatContext", 94 | format: null, 95 | }, 96 | ], 97 | "@typescript-eslint/no-empty-interface": "error", 98 | "@typescript-eslint/no-floating-promises": "error", 99 | "@typescript-eslint/no-misused-new": "error", 100 | "@typescript-eslint/no-namespace": "error", 101 | "@typescript-eslint/no-redeclare": "error", 102 | "@typescript-eslint/no-shadow": [ 103 | "error", 104 | { 105 | hoist: "all", 106 | }, 107 | ], 108 | "@typescript-eslint/no-this-alias": "error", 109 | "@typescript-eslint/no-unused-expressions": "error", 110 | "@typescript-eslint/no-unused-vars": [ 111 | "error", 112 | { 113 | argsIgnorePattern: "^_", 114 | varsIgnorePattern: "^_", 115 | }, 116 | ], 117 | "@typescript-eslint/prefer-for-of": "error", 118 | "@typescript-eslint/prefer-function-type": "error", 119 | "@typescript-eslint/prefer-namespace-keyword": "error", 120 | "@typescript-eslint/restrict-plus-operands": "error", 121 | "@typescript-eslint/restrict-template-expressions": [ 122 | "error", 123 | { 124 | allowAny: true, 125 | }, 126 | ], 127 | "@typescript-eslint/triple-slash-reference": [ 128 | "error", 129 | { 130 | path: "always", 131 | types: "prefer-import", 132 | lib: "always", 133 | }, 134 | ], 135 | "@typescript-eslint/unified-signatures": "error", 136 | "constructor-super": "error", 137 | "eqeqeq": ["error", "always"], 138 | "guard-for-in": "error", 139 | "id-blacklist": "error", 140 | "id-match": "error", 141 | "import/no-extraneous-dependencies": [ 142 | "error", 143 | { 144 | devDependencies: false, 145 | }, 146 | ], 147 | "import/order": [ 148 | "error", 149 | { 150 | groups: [ 151 | "type", 152 | "object", 153 | ["builtin", "external"], 154 | "parent", 155 | "sibling", 156 | "index", 157 | ], 158 | }, 159 | ], 160 | "import/no-default-export": "error", 161 | "no-bitwise": "error", 162 | "no-caller": "error", 163 | "no-cond-assign": "error", 164 | "no-debugger": "error", 165 | "no-duplicate-case": "error", 166 | "@typescript-eslint/no-duplicate-imports": "error", 167 | "no-eval": "error", 168 | "no-extra-bind": "error", 169 | "no-new-func": "error", 170 | "no-new-wrappers": "error", 171 | "no-only-tests/no-only-tests": "error", 172 | "no-return-await": "off", 173 | "@typescript-eslint/return-await": "error", 174 | "no-sequences": "error", 175 | "no-sparse-arrays": "error", 176 | "no-template-curly-in-string": "error", 177 | "no-throw-literal": "error", 178 | "no-undef-init": "error", 179 | "no-unsafe-finally": "error", 180 | "no-unused-labels": "error", 181 | "no-unused-vars": "off", 182 | "no-var": "error", 183 | "object-shorthand": "error", 184 | "one-var": ["error", "never"], 185 | "prefer-const": "error", 186 | "prefer-object-spread": "error", 187 | "prefer-template": "error", 188 | "spaced-comment": [ 189 | "error", 190 | "always", 191 | { 192 | markers: ["/"], 193 | }, 194 | ], 195 | "use-isnan": "error" 196 | }, 197 | }; -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0xaA4c632684180bf781108c84E7a294B483D12053" 5 | } 6 | }, 7 | "opRetro": { 8 | "projectId": "0xc2d8139cd61ddc75689610ee0cca0fe37ba8bec3270beb6aac6eac91c6256f60" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-gas-reporter", 3 | "version": "2.3.0", 4 | "description": "Gas Analytics plugin for Hardhat", 5 | "repository": "github:cgewecke/hardhat-gas-reporter", 6 | "author": "cgewecke", 7 | "license": "MIT", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "keywords": [ 11 | "ethereum", 12 | "smart-contracts", 13 | "hardhat", 14 | "hardhat-plugin", 15 | "unit tests", 16 | "gas" 17 | ], 18 | "scripts": { 19 | "lint": "npm run eslint", 20 | "lint:fix": "npm run eslint -- --fix", 21 | "eslint": "eslint 'src/**/*.ts' 'test/**/*.ts' -c ./eslintrc.js", 22 | "gen:options": "ts-node scripts/gen-options-md.ts", 23 | "test": "scripts/run-tests.sh", 24 | "test:integration": "mocha test/integration/*.ts --timeout 100000 --exit", 25 | "test:unit": "mocha test/unit/*.ts --timeout 10000", 26 | "prepublishOnly": "tsc --project tsconfig.prod.json", 27 | "build": "tsc --project tsconfig.prod.json", 28 | "buidl": "tsc", 29 | "watch": "tsc -w" 30 | }, 31 | "files": [ 32 | "dist/", 33 | "src/", 34 | "LICENSE", 35 | "README.md" 36 | ], 37 | "resolutions": { 38 | "glob/wrap-ansi": "7.0.0" 39 | }, 40 | "devDependencies": { 41 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 42 | "@nomicfoundation/hardhat-network-helpers": "^1.0.10", 43 | "@nomicfoundation/hardhat-verify": "^2.0.0", 44 | "@nomicfoundation/hardhat-viem": "^2.0.0", 45 | "@openzeppelin/contracts": "3.4.1-solc-0.7-2", 46 | "@openzeppelin/contracts-upgradeable": "^5.0.1", 47 | "@openzeppelin/hardhat-upgrades": "^3.0.4", 48 | "@types/chai": "^4.2.14", 49 | "@types/chai-as-promised": "7.1.8", 50 | "@types/fs-extra": "^5.0.4", 51 | "@types/lodash": "^4.14.202", 52 | "@types/markdown-table": "2.0.0", 53 | "@types/mocha": "7", 54 | "@types/node": "^8.10.38", 55 | "@types/sha1": "^1.1.5", 56 | "@typescript-eslint/eslint-plugin": "5.61.0", 57 | "@typescript-eslint/parser": "5.61.0", 58 | "@uniswap/v3-core": "1.0.0", 59 | "@uniswap/v3-periphery": "1.1.1", 60 | "chai": "^4.2.0", 61 | "chai-as-promised": "7.1.1", 62 | "dotenv": "^6.2.0", 63 | "eslint": "8.44.0", 64 | "eslint-plugin-import": "2.27.5", 65 | "eslint-plugin-no-only-tests": "3.0.0", 66 | "ethers": "^6.11.1", 67 | "hardhat": "^2.22.19", 68 | "mocha": "10.0.0", 69 | "prettier": "2.4.1", 70 | "source-map-support": "^0.5.12", 71 | "ts-node": "^8.1.0", 72 | "typescript": "~5.0.4" 73 | }, 74 | "peerDependencies": { 75 | "hardhat": "^2.16.0" 76 | }, 77 | "dependencies": { 78 | "@ethersproject/abi": "^5.7.0", 79 | "@ethersproject/bytes": "^5.7.0", 80 | "@ethersproject/units": "^5.7.0", 81 | "@solidity-parser/parser": "^0.20.1", 82 | "axios": "^1.6.7", 83 | "brotli-wasm": "^2.0.1", 84 | "chalk": "4.1.2", 85 | "cli-table3": "^0.6.3", 86 | "ethereum-cryptography": "^2.1.3", 87 | "glob": "^10.3.10", 88 | "jsonschema": "^1.4.1", 89 | "lodash": "^4.17.21", 90 | "markdown-table": "2.0.0", 91 | "sha1": "^1.1.1", 92 | "viem": "^2.27.0" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | trap cleanup EXIT 5 | 6 | cleanup() { 7 | # Github actions kills the process 8 | if [ -n "$CI" ]; then 9 | echo "Exiting without killing Hardhat Node in CI" 10 | return 11 | fi 12 | 13 | if [ -n "$hardhat_node_pid" ] && ps -p $hardhat_node_pid > /dev/null; then 14 | echo "Killing Hardhat Node." 15 | kill -9 $hardhat_node_pid 16 | fi 17 | } 18 | 19 | start_hardhat_node() { 20 | echo "Launching Hardhat Node..." 21 | node_modules/.bin/hardhat node > /dev/null & 22 | hardhat_node_pid=$! 23 | sleep 4 24 | } 25 | 26 | ######## 27 | # Units 28 | ######## 29 | npx mocha test/unit/*.ts --timeout 10000 30 | 31 | ######## 32 | # Tasks 33 | ######## 34 | npx mocha test/tasks/merge.ts --timeout 10000 35 | 36 | ################################ 37 | # Hardhat EVM (Default Network) 38 | ################################ 39 | npx mocha test/integration/*.ts --timeout 100000 --exit 40 | 41 | ########################## 42 | # Hardhat Node (Localhost) 43 | ########################## 44 | start_hardhat_node 45 | STAND_ALONE=true npx mocha test/integration/node.ts --timeout 100000 --exit 46 | 47 | cleanup 48 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const TABLE_NAME_LEGACY = "legacy"; 2 | export const TABLE_NAME_TERMINAL = "terminal"; 3 | export const TABLE_NAME_MARKDOWN = "markdown"; 4 | 5 | export const DEFAULT_CURRENCY = "USD"; 6 | export const DEFAULT_CURRENCY_DISPLAY_PRECISION = 2; 7 | export const DEFAULT_JSON_OUTPUT_FILE = "./gasReporterOutput.json"; 8 | export const DEFAULT_GAS_PRICE_PRECISION = 5; 9 | 10 | // Selector generated with: ethersV5.Interface.encodeFunctionData("blobBaseFee()", []); 11 | export const DEFAULT_BLOB_BASE_FEE_API_ARGS = "action=eth_call&data=0xf8206140&tag=latest&to=" 12 | 13 | export const DEFAULT_GET_BLOCK_API_ARGS = "action=eth_getBlockByNumber&tag=latest&boolean=false" 14 | export const DEFAULT_GAS_PRICE_API_ARGS = "action=eth_gasPrice" 15 | export const DEFAULT_API_KEY_ARGS = "&apikey=" 16 | export const DEFAULT_COINMARKET_BASE_URL = "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/" 17 | 18 | export const DEFAULT_OPTIMISM_HARDFORK = "ecotone"; 19 | export const DEFAULT_ARBITRUM_HARDFORK = "arbOS11"; 20 | 21 | export const TOOLCHAIN_HARDHAT = "hardhat"; 22 | export const TOOLCHAIN_FOUNDRY = "foundry"; 23 | 24 | export const CACHE_FILE_NAME = ".hardhat_gas_reporter_output.json"; 25 | 26 | // EVM 27 | export const EVM_BASE_TX_COST = 21000; 28 | export const DEFAULT_BLOB_BASE_FEE = 10; // gwei 29 | 30 | // Source: 31 | // https://docs.optimism.io/stack/transactions/fees#bedrock 32 | export const OPTIMISM_BEDROCK_FIXED_OVERHEAD = 188; 33 | export const OPTIMISM_BEDROCK_DYNAMIC_OVERHEAD = 0.684; 34 | 35 | // These params are configured by node operators and may vary 36 | // Values were read from the GasPriceOracle contract at: 37 | // https://optimistic.etherscan.io/address/0x420000000000000000000000000000000000000F 38 | export const OPTIMISM_ECOTONE_BASE_FEE_SCALAR = 1368 39 | export const OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR = 810949 40 | 41 | // https://basescan.org/address/0x420000000000000000000000000000000000000F 42 | export const BASE_ECOTONE_BASE_FEE_SCALAR = 1101; 43 | export const BASE_ECOTONE_BLOB_BASE_FEE_SCALAR = 659851; 44 | 45 | export const UNICODE_CIRCLE = "◯"; 46 | export const UNICODE_TRIANGLE = "△" 47 | 48 | export const RANDOM_R_COMPONENT = "0x12354631f8e7f6d04a0f71b4e2a7b50b165ad2e50a83d531cbd88587b4bd62d5"; 49 | export const RANDOM_S_COMPONENT = "0x49cd68893c5952ea1e00288b05699be582081c5fba8c2c6f6e90dd416cdc2e07"; 50 | 51 | /** 52 | * Generated with: 53 | * 54 | * erc20Calldata = ethersV5.Interface.encodeFunctionData("decimals()", []) 55 | * 56 | * ethersV5.Interface.encodeFunctionData( 57 | * "gasEstimateL1Component(address to, bool contractCreation, bytes calldata data)", 58 | * [ 59 | * "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC 60 | * false, 61 | * erc20Calldata 62 | * ] 63 | * ); 64 | * 65 | */ 66 | export const ARBITRUM_L1_ESTIMATE_CALLDATA = "0x77d488a2000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004313ce56700000000000000000000000000000000000000000000000000000000"; 67 | 68 | // Source: @arbitrum/sdk/dist/lib/dataEntities/constants 69 | export const ARBITRUM_NODE_INTERFACE_ADDRESS = "0x00000000000000000000000000000000000000C8"; 70 | 71 | export const DEFAULT_BASE_FEE_PER_BYTE_API_ARGS = 72 | `action=eth_call&data=${ARBITRUM_L1_ESTIMATE_CALLDATA}&tag=latest&to=${ARBITRUM_NODE_INTERFACE_ADDRESS}`; 73 | 74 | export const OPTIMISM_GAS_ORACLE_ABI_PARTIAL = [ 75 | { 76 | constant: true, 77 | inputs: [], 78 | name: "blobBaseFee", 79 | outputs: [ 80 | { 81 | name: "", 82 | type: "uint256", 83 | }, 84 | ], 85 | payable: false, 86 | stateMutability: "view", 87 | type: "function", 88 | }, 89 | { 90 | constant: true, 91 | inputs: [], 92 | name: "baseFeeScalar", 93 | outputs: [ 94 | { 95 | name: "", 96 | type: "uint32", 97 | }, 98 | ], 99 | payable: false, 100 | stateMutability: "view", 101 | type: "function", 102 | }, 103 | { 104 | constant: true, 105 | inputs: [], 106 | name: "blobBaseFeeScalar", 107 | outputs: [ 108 | { 109 | name: "", 110 | type: "uint32", 111 | }, 112 | ], 113 | payable: false, 114 | stateMutability: "view", 115 | type: "function", 116 | }]; 117 | 118 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from "lodash" // used in extendConfig, cannot await import 2 | import { EIP1193Provider, HardhatConfig, HardhatUserConfig } from "hardhat/types"; 3 | import { TASK_TEST, TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; 4 | import { 5 | extendConfig, 6 | extendEnvironment, 7 | extendProvider, 8 | task, 9 | subtask 10 | } from "hardhat/config"; 11 | 12 | import "./type-extensions"; 13 | import { GasReporterExecutionContext, GasReporterOutput } from "./types"; 14 | 15 | import { getDefaultOptions } from './lib/options'; 16 | import { GasReporterProvider } from "./lib/provider"; 17 | import { 18 | TASK_GAS_REPORTER_MERGE, 19 | TASK_GAS_REPORTER_MERGE_REPORTS, 20 | TASK_GAS_REPORTER_MERGE_LEGACY, 21 | TASK_GAS_REPORTER_MERGE_REPORTS_LEGACY, 22 | TASK_GAS_REPORTER_START, 23 | TASK_GAS_REPORTER_STOP 24 | } from "./task-names" 25 | 26 | let _globalGasReporterProviderReference: GasReporterProvider; 27 | 28 | // ======================== 29 | // EXTENSIONS 30 | // ======================== 31 | /* Config */ 32 | extendConfig( 33 | (config: HardhatConfig, userConfig: Readonly) => { 34 | let options = getDefaultOptions(userConfig); 35 | 36 | // Deep clone userConfig otherwise HH will throw unauthorized modification error 37 | if (userConfig.gasReporter !== undefined) { 38 | options = Object.assign(options, cloneDeep(userConfig.gasReporter)); 39 | 40 | // Use legacy Etherscan API Key if user did not migrate from deprecated options 41 | if (options.L1Etherscan && !options.etherscan) { 42 | options.etherscan = options.L1Etherscan 43 | } 44 | } 45 | 46 | (config as any).gasReporter = options; 47 | } 48 | ); 49 | 50 | /* Environment */ 51 | extendEnvironment((hre) => { 52 | hre.__hhgrec = { 53 | collector: undefined, 54 | task: undefined, 55 | } 56 | }); 57 | 58 | /* Provider */ 59 | extendProvider(async (provider) => { 60 | const newProvider = new GasReporterProvider(provider); 61 | _globalGasReporterProviderReference = newProvider; 62 | return newProvider; 63 | }); 64 | 65 | /* 66 | Initialize the provider with the execution context. This is called in 67 | `TASK_GAS_REPORTER_START` at the very end of setup. Provider extension above should 68 | not be used on unrelated tasks. 69 | */ 70 | export async function initializeGasReporterProvider( 71 | provider: EIP1193Provider, 72 | context: GasReporterExecutionContext 73 | ) { 74 | // Other plugins (ex: hardhat-tracer) may wrap the provider in a way 75 | // that doesn't expose `init()`, so we init the underlying provider 76 | // here by making a cheap call. 77 | await provider.request({ method: "eth_blockNumber", params: []}); 78 | _globalGasReporterProviderReference.initializeGasReporterProvider(context); 79 | } 80 | 81 | // ======================== 82 | // BUILT-IN OVERRIDES 83 | // ======================== 84 | 85 | /** 86 | * Overrides Hardhat built-in task TASK_TEST to report gas usage 87 | */ 88 | task(TASK_TEST).setAction( 89 | async (args: any, hre, runSuper) => { 90 | hre.__hhgrec.task = TASK_TEST; 91 | await hre.run(TASK_GAS_REPORTER_START, args); 92 | await runSuper(args); 93 | await hre.run(TASK_GAS_REPORTER_STOP, args); 94 | } 95 | ); 96 | 97 | // ======================== 98 | // GAS REPORTER TASKS 99 | // ======================== 100 | 101 | /** 102 | * Initializes gas tracking 103 | */ 104 | subtask(TASK_GAS_REPORTER_START).setAction( 105 | async (args: any, hre) => { 106 | const options = hre.config.gasReporter; 107 | 108 | if (options.enabled === true) { 109 | // Lazy load all imports to minimize HH startup time 110 | const { getContracts } = await import("./lib/artifacts"); 111 | const { Collector } = await import("./lib/collector"); 112 | const { warnParallel } = await import("./utils/ui"); 113 | 114 | // Temporarily skipping when in parallel mode because it crashes and 115 | // unsure how to resolve... 116 | if (args.parallel === true) { 117 | warnParallel(); 118 | return; 119 | } 120 | 121 | // solidity-coverage disables gas reporter via mocha but that 122 | // no longer works for this version. (No warning necessary) 123 | if ((hre as any).__SOLIDITY_COVERAGE_RUNNING === true) { 124 | return; 125 | } 126 | 127 | // Need to compile so we have access to the artifact data. 128 | // This will rerun in TASK_TEST & TASK_RUN but should be a noop there. 129 | if (!args.noCompile) { 130 | await hre.run(TASK_COMPILE, { quiet: true }); 131 | } 132 | 133 | const contracts = await getContracts(hre, options); 134 | 135 | hre.__hhgrec.usingCall = options.reportPureAndViewMethods; 136 | hre.__hhgrec.usingViem = (hre as any).viem; 137 | hre.__hhgrec.usingOZ = (hre as any).upgrades || (hre as any).defender 138 | 139 | hre.__hhgrec.collector = new Collector(hre, options); 140 | hre.__hhgrec.collector.data.initialize(hre.network.provider, contracts); 141 | 142 | // Custom proxy resolvers are instantiated in the config, 143 | // OZ proxy resolver instantiated in Resolver constructor called by new Collector() 144 | hre.__hhgrec.methodIgnoreList = (options.proxyResolver) 145 | ? options.proxyResolver.ignore() 146 | : []; 147 | 148 | await initializeGasReporterProvider(hre.network.provider, hre.__hhgrec); 149 | } 150 | } 151 | ); 152 | 153 | /** 154 | * Completes gas reporting: gets live market data, runs analysis and renders 155 | */ 156 | subtask(TASK_GAS_REPORTER_STOP).setAction( 157 | async (args: any, hre) => { 158 | const options = hre.config.gasReporter; 159 | 160 | if ( 161 | options.enabled === true && 162 | args.parallel !== true && 163 | (hre as any).__SOLIDITY_COVERAGE_RUNNING !== true 164 | ) { 165 | const { setGasAndPriceRates } = await import("./utils/prices"); 166 | const { render } = await import("./lib/render"); 167 | 168 | const warnings = await setGasAndPriceRates(options); 169 | 170 | await hre.__hhgrec.collector?.data.runAnalysis(hre, options); 171 | render(hre, options, warnings); 172 | } 173 | } 174 | ); 175 | 176 | /** 177 | * ======================== 178 | * CLI COMMAND TASKS 179 | * ======================== 180 | */ 181 | 182 | subtask(TASK_GAS_REPORTER_MERGE_REPORTS) 183 | .addOptionalVariadicPositionalParam( 184 | "inputFiles", 185 | "Path of several gasReporterOutput.json files to merge", 186 | [] 187 | ) 188 | .setAction( 189 | async ({ inputFiles }: { inputFiles: string[] } 190 | ): Promise => { 191 | const { subtaskMergeReportsImplementation } = await import("./tasks/mergeReports") 192 | return subtaskMergeReportsImplementation({ inputFiles }) 193 | }); 194 | 195 | task(TASK_GAS_REPORTER_MERGE) 196 | .addOptionalParam( 197 | "output", 198 | "Target file to save the merged report", 199 | "gasReporterOutput.json" 200 | ) 201 | .addVariadicPositionalParam( 202 | "input", 203 | "A list of JSON data files generated by the gas reporter plugin. " + 204 | "Files can be defined using glob patterns" 205 | ) 206 | .setAction(async (taskArguments, hre) => { 207 | const { taskMergeImplementation } = await import("./tasks/mergeReports") 208 | return taskMergeImplementation(taskArguments, hre); 209 | }); 210 | 211 | /** 212 | * ======================== 213 | * DEPRECATED TASKS 214 | * ======================== 215 | */ 216 | task(TASK_GAS_REPORTER_MERGE_LEGACY) 217 | .addOptionalParam( 218 | "output", 219 | "Target file to save the merged report", 220 | "gasReporterOutput.json" 221 | ) 222 | .addVariadicPositionalParam("input") 223 | .setAction(async () => { 224 | const { warnDeprecatedTask } = await import("./utils/ui"); 225 | warnDeprecatedTask(TASK_GAS_REPORTER_MERGE) 226 | }); 227 | 228 | subtask(TASK_GAS_REPORTER_MERGE_REPORTS_LEGACY) 229 | .addOptionalVariadicPositionalParam("inputFiles", "", []) 230 | .setAction(async ({}: { inputFiles: string[] }) => { 231 | const { warnDeprecatedTask } = await import("./utils/ui"); 232 | warnDeprecatedTask(TASK_GAS_REPORTER_MERGE_REPORTS); 233 | }); 234 | 235 | -------------------------------------------------------------------------------- /src/lib/artifacts.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import _ from "lodash"; 3 | import {parse, visit} from "@solidity-parser/parser"; 4 | import { Interface } from "@ethersproject/abi"; 5 | 6 | import { EthereumProvider, HardhatRuntimeEnvironment } from "hardhat/types"; 7 | import { getHashedFunctionSignature } from "../utils/sources"; 8 | 9 | import { RemoteContract, ContractInfo, GasReporterOptions } from "../types"; 10 | 11 | /** 12 | * Filters out contracts to exclude from report 13 | * @param {string} qualifiedName HRE artifact identifier 14 | * @param {string[]} skippable excludeContracts option values 15 | * @return {boolean} 16 | */ 17 | function shouldSkipContract( 18 | qualifiedName: string, 19 | skippable: string[] 20 | ): boolean { 21 | for (const item of skippable) { 22 | if (qualifiedName.includes(item)) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | /** 30 | * Fetches remote bytecode at address and hashes it so these addresses can be 31 | * added to the tracking in the collector 32 | * @param {EGRAsyncApiProvider} provider 33 | * @param {RemoteContract[] = []} remoteContracts 34 | * @return {Promise} 35 | */ 36 | export async function getResolvedRemoteContracts( 37 | provider: EthereumProvider, 38 | remoteContracts: RemoteContract[] = [] 39 | ): Promise { 40 | const { default: sha1 } = await import("sha1"); 41 | for (const contract of remoteContracts) { 42 | try { 43 | contract.bytecode = await provider.send("eth_getCode", [contract.address, "latest"]); 44 | contract.deployedBytecode = contract.bytecode; 45 | contract.bytecodeHash = sha1(contract.bytecode!); 46 | } catch (error: any) { 47 | console.log( 48 | `hardhat-gas-reporter:warning: failed to fetch bytecode for remote contract: ${contract.name}` 49 | ); 50 | console.log(`Error was: ${error}\n`); 51 | } 52 | } 53 | return remoteContracts; 54 | } 55 | 56 | /** 57 | * Loads and processes artifacts 58 | * @param {HardhatRuntimeEnvironment} hre 59 | * @param {GasReporterOptions[]} options 60 | * @return {ContractInfo[]} 61 | */ 62 | export async function getContracts( 63 | hre: HardhatRuntimeEnvironment, 64 | options: GasReporterOptions, 65 | ): Promise { 66 | const visited = {}; 67 | const contracts: ContractInfo[] = []; 68 | 69 | const resolvedRemoteContracts = await getResolvedRemoteContracts( 70 | hre.network.provider, 71 | options.remoteContracts 72 | ); 73 | 74 | const resolvedQualifiedNames = await hre.artifacts.getAllFullyQualifiedNames(); 75 | 76 | for (const qualifiedName of resolvedQualifiedNames) { 77 | if (shouldSkipContract(qualifiedName, options.excludeContracts!)) { 78 | continue; 79 | } 80 | 81 | let name: string; 82 | let artifact = await hre.artifacts.readArtifact(qualifiedName); 83 | 84 | // Prefer simple names 85 | try { 86 | artifact = await hre.artifacts.readArtifact(artifact.contractName); 87 | name = artifact.contractName; 88 | } catch (e) { 89 | name = path.relative(hre.config.paths.sources, qualifiedName);; 90 | } 91 | 92 | const excludedMethods = await getExcludedMethodKeys( 93 | hre, 94 | options, 95 | artifact.abi, 96 | name, 97 | qualifiedName, 98 | visited 99 | ); 100 | 101 | contracts.push({ 102 | name, 103 | excludedMethods, 104 | artifact: { 105 | abi: artifact.abi, 106 | bytecode: artifact.bytecode, 107 | deployedBytecode: artifact.deployedBytecode, 108 | }, 109 | }); 110 | } 111 | 112 | for (const remoteContract of resolvedRemoteContracts) { 113 | contracts.push({ 114 | name: remoteContract.name, 115 | excludedMethods: [], // no source 116 | artifact: { 117 | abi: remoteContract.abi, 118 | address: remoteContract.address, 119 | bytecode: remoteContract.bytecode, 120 | bytecodeHash: remoteContract.bytecodeHash, 121 | deployedBytecode: remoteContract.deployedBytecode, 122 | }, 123 | } as ContractInfo); 124 | } 125 | return contracts; 126 | } 127 | 128 | /** 129 | * Parses each file in a contract's dependency tree to identify public StateVariables and 130 | * add them to a list of methods to exclude from the report. Enabled when 131 | * `excludeAutoGeneratedGetters` and `reportPureAndViewMethods` are both true. 132 | * 133 | * TODO: warn when files don't parse 134 | * 135 | * @param {HardhatRuntimeEnvironment} hre 136 | * @param {GasReporterOptions} options 137 | * @param {any[]} abi 138 | * @param {string} name 139 | * @param {string} qualifiedName 140 | * @param {[key: string]: string[]} visited (cache) 141 | * @returns {Promise} 142 | */ 143 | async function getExcludedMethodKeys( 144 | hre: HardhatRuntimeEnvironment, 145 | options: GasReporterOptions, 146 | abi: any[], 147 | contractName: string, 148 | contractQualifiedName: string, 149 | visited: {[key: string]: string[]} 150 | ): Promise { 151 | const excludedMethods = new Set(); 152 | 153 | if (options.reportPureAndViewMethods && options.excludeAutoGeneratedGetters) { 154 | const info = await hre.artifacts.getBuildInfo(contractQualifiedName); 155 | const functions = new Interface(abi).functions 156 | 157 | if (info && info.input && info.input.sources) { 158 | _.forEach(info?.input.sources, (source, sourceKey) => { 159 | // Cache dependency sources 160 | if (!visited[sourceKey]){ 161 | visited[sourceKey] = []; 162 | } else { 163 | visited[sourceKey].forEach(_name => { 164 | if (!excludedMethods.has(_name)){ 165 | excludedMethods.add(`${contractName}_${getHashedFunctionSignature(_name)}`) 166 | } 167 | }) 168 | return; 169 | }; 170 | 171 | try { 172 | const ast = parse(source.content, {tolerant: true}); 173 | visit(ast, { 174 | StateVariableDeclaration (node) { 175 | const publicVars = node.variables.filter(({ visibility }) => visibility === 'public'); 176 | publicVars.forEach(_var => { 177 | const formattedName = Object.keys(functions).find(key => functions[key].name === _var.name); 178 | if (formattedName){ 179 | visited[sourceKey].push(formattedName); 180 | excludedMethods.add(`${contractName}_${getHashedFunctionSignature(formattedName)}`) 181 | } 182 | }) 183 | } 184 | }) 185 | } catch (err) { /* ignore */ } 186 | }); 187 | } 188 | } 189 | return Array.from(excludedMethods) as string[]; 190 | } 191 | -------------------------------------------------------------------------------- /src/lib/collector.ts: -------------------------------------------------------------------------------- 1 | import type { RpcReceiptOutput } from "hardhat/internal/hardhat-network/provider/output" 2 | import { hexlify } from "@ethersproject/bytes"; 3 | 4 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 5 | import { getMethodID } from "../utils/sources"; 6 | import { 7 | getCalldataGasForNetwork, 8 | hexToDecimal, 9 | getIntrinsicGas, 10 | getGasSubIntrinsic 11 | } from "../utils/gas"; 12 | import { GasReporterOptions, JsonRpcTx, FakeTx, ValidatedRequestArguments } from "../types" 13 | import { GasData } from "./gasData"; 14 | import { Resolver } from "./resolvers"; 15 | 16 | 17 | /** 18 | * Collects gas usage data, associating it with the relevant contracts, methods. 19 | */ 20 | export class Collector { 21 | public data: GasData; 22 | public options: GasReporterOptions; 23 | public resolver: Resolver; 24 | 25 | constructor(hre: HardhatRuntimeEnvironment, options: GasReporterOptions) { 26 | this.data = new GasData(); 27 | this.options = options; 28 | this.resolver = new Resolver(hre, options, this.data); 29 | } 30 | 31 | /** 32 | * Method called by the request monitor on the provider to collect deployment 33 | * and methods gas data 34 | * @param {JsonRpcTx} transaction [description] 35 | * @param {TransactionReceipt} receipt [description] 36 | */ 37 | public async collectTransaction(tx: JsonRpcTx, receipt: RpcReceiptOutput): Promise { 38 | if (receipt.contractAddress !== null) 39 | await this._collectDeploymentsData(tx, receipt); 40 | else 41 | await this._collectMethodsData(tx, receipt, false); 42 | } 43 | 44 | /** 45 | * Method called by the request monitor on the provider to get gas data for `eth_call` 46 | * @param {ValidatedRequestArguments} params.args of the call 47 | * @param {number} estimate_gas result 48 | */ 49 | public async collectCall(args: ValidatedRequestArguments, gas: number): Promise { 50 | const callGas = gas - getIntrinsicGas(args.params[0].data); 51 | const fakeTx = { 52 | input: args.params[0].data, 53 | to: args.params[0].to, 54 | isCall: true 55 | } 56 | 57 | const fakeReceipt = { 58 | gasUsed: hexlify(callGas) 59 | } 60 | 61 | await this._collectMethodsData( 62 | fakeTx as FakeTx, 63 | fakeReceipt as unknown as RpcReceiptOutput, 64 | true 65 | ); 66 | } 67 | 68 | /** 69 | * Extracts and stores deployments gas usage data for a tx 70 | * @param {JsonRpcTx} tx return value of `getTransactionByHash` 71 | * @param {TransactionReceipt} receipt 72 | */ 73 | private async _collectDeploymentsData(tx: JsonRpcTx, receipt: RpcReceiptOutput): Promise { 74 | const match = this.data.getContractByDeploymentInput(tx.input!); 75 | 76 | if (match !== null) { 77 | await this.data.trackNameByAddress( 78 | match.name, 79 | receipt.contractAddress! 80 | ); 81 | 82 | const executionGas = hexToDecimal(receipt.gasUsed); 83 | const calldataGas = getCalldataGasForNetwork(this.options, tx); 84 | 85 | match.gasData.push(executionGas); 86 | match.callData.push(calldataGas); 87 | } 88 | } 89 | 90 | /** 91 | * Extracts and stores methods gas usage data for a tx 92 | * @param {JsonRpcTx} transaction return value of `getTransactionByHash` 93 | * @param {TransactionReceipt} receipt 94 | * @param {boolean} isCall 95 | */ 96 | private async _collectMethodsData( 97 | tx: JsonRpcTx | FakeTx, 98 | receipt: RpcReceiptOutput, 99 | isCall: boolean 100 | ): Promise { 101 | let contractName = await this.data.getNameByAddress(tx.to); 102 | 103 | // Case: proxied call 104 | if (this._isProxied(contractName, tx.input!)) { 105 | contractName = await this.resolver.resolveByProxy(tx); 106 | } 107 | 108 | // Case: hidden contract factory deployment 109 | if (contractName === null) { 110 | contractName = await this.resolver.resolveByDeployedBytecode( 111 | tx.to 112 | ); 113 | } 114 | 115 | // Case: all else fails, use first match strategy 116 | if (contractName === null) { 117 | contractName = this.resolver.resolveByMethodSignature(tx as JsonRpcTx); 118 | } 119 | 120 | const id = getMethodID(contractName!, tx.input!); 121 | 122 | if (this.data.methods[id] !== undefined) { 123 | const executionGas = (this.options.includeIntrinsicGas) 124 | ? hexToDecimal(receipt.gasUsed) 125 | : getGasSubIntrinsic(tx.input, hexToDecimal(receipt.gasUsed)); 126 | 127 | // If L2 txs have intrinsic turned off, we assume caller 128 | // is paying the L1 overhead 129 | const calldataGas = (isCall || !this.options.includeIntrinsicGas) 130 | ? 0 131 | : getCalldataGasForNetwork(this.options, tx as JsonRpcTx); 132 | 133 | const intrinsicGas = getIntrinsicGas(tx.input); 134 | 135 | this.data.methods[id].gasData.push(executionGas); 136 | this.data.methods[id].callData.push(calldataGas); 137 | this.data.methods[id].intrinsicGas.push(intrinsicGas); 138 | this.data.methods[id].numberOfCalls += 1; 139 | this.data.methods[id].isCall = this.data.methods[id].isCall || !this.options.includeIntrinsicGas; 140 | } else { 141 | this.resolver.unresolvedCalls++; 142 | } 143 | } 144 | 145 | /** 146 | * Returns true if there is a contract name associated with an address 147 | * but method can't be matched to it 148 | * @param {String} name contract name 149 | * @param {String} input code 150 | * @return {Boolean} 151 | */ 152 | private _isProxied(name: string | null, input: string): boolean { 153 | if (name !== null) { 154 | return (this.data.methods[getMethodID(name, input)] === undefined) 155 | } 156 | return false; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/lib/options.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/types"; 2 | import { 3 | DEFAULT_CURRENCY, 4 | DEFAULT_CURRENCY_DISPLAY_PRECISION, 5 | DEFAULT_JSON_OUTPUT_FILE, 6 | DEFAULT_OPTIMISM_HARDFORK, 7 | BASE_ECOTONE_BASE_FEE_SCALAR, 8 | BASE_ECOTONE_BLOB_BASE_FEE_SCALAR, 9 | OPTIMISM_ECOTONE_BASE_FEE_SCALAR, 10 | OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR, 11 | TABLE_NAME_TERMINAL 12 | } from "../constants"; 13 | 14 | import { GasReporterOptions, OptimismHardfork } from "../types"; 15 | 16 | /** 17 | * Validates Optimism hardfork option 18 | * @param hardfork 19 | * @returns {boolean} 20 | */ 21 | function isOptimismHardfork(hardfork: string | undefined) { 22 | if (hardfork === undefined) return false; 23 | 24 | return ["bedrock, ecotone"].includes(hardfork); 25 | } 26 | 27 | /** 28 | * Sets default reporter options 29 | */ 30 | export function getDefaultOptions(userConfig: Readonly): GasReporterOptions { 31 | let optimismHardfork: OptimismHardfork; 32 | let opStackBaseFeeScalar: number = 0; 33 | let opStackBlobBaseFeeScalar: number = 0; 34 | 35 | const userOptions = userConfig.gasReporter; 36 | 37 | // NB: silently coercing to default if there's a misspelling or option not avail 38 | if (userOptions) { 39 | if (userOptions.L2 === "optimism" || userOptions.L2 === "base") 40 | if (!isOptimismHardfork(userOptions.optimismHardfork)){ 41 | optimismHardfork = DEFAULT_OPTIMISM_HARDFORK; 42 | } 43 | 44 | if (userOptions.L2 === "optimism") { 45 | if (!userOptions.opStackBaseFeeScalar) { 46 | opStackBaseFeeScalar = OPTIMISM_ECOTONE_BASE_FEE_SCALAR; 47 | } 48 | if (!userOptions.opStackBlobBaseFeeScalar) { 49 | opStackBlobBaseFeeScalar = OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR 50 | } 51 | } 52 | 53 | if (userOptions.L2 === "base") { 54 | if (!userOptions.opStackBaseFeeScalar) { 55 | opStackBaseFeeScalar = BASE_ECOTONE_BASE_FEE_SCALAR; 56 | } 57 | if (!userOptions.opStackBlobBaseFeeScalar) { 58 | opStackBlobBaseFeeScalar = BASE_ECOTONE_BLOB_BASE_FEE_SCALAR 59 | } 60 | } 61 | } 62 | 63 | return { 64 | currency: DEFAULT_CURRENCY, 65 | currencyDisplayPrecision: DEFAULT_CURRENCY_DISPLAY_PRECISION, 66 | darkMode: false, 67 | enabled: true, 68 | excludeContracts: [], 69 | excludeAutoGeneratedGetters: false, 70 | forceTerminalOutput: false, 71 | includeBytecodeInJSON: false, 72 | includeIntrinsicGas: true, 73 | L1: "ethereum", 74 | noColors: false, 75 | offline: false, 76 | opStackBaseFeeScalar, 77 | opStackBlobBaseFeeScalar, 78 | optimismHardfork, 79 | outputJSON: false, 80 | outputJSONFile: DEFAULT_JSON_OUTPUT_FILE, 81 | reportFormat: TABLE_NAME_TERMINAL, 82 | reportPureAndViewMethods: false, 83 | rst: false, 84 | rstTitle: "", 85 | suppressTerminalOutput: false, 86 | showMethodSig: false, 87 | showUncalledMethods: false, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/provider.ts: -------------------------------------------------------------------------------- 1 | import { ProviderWrapper } from "hardhat/plugins"; 2 | 3 | import { EIP1193Provider, RequestArguments } from "hardhat/types"; 4 | import { hexToDecimal } from "../utils/gas"; 5 | 6 | import { GasReporterExecutionContext, JsonRpcTx, ValidatedRequestArguments } from "../types"; 7 | 8 | /** 9 | * Wrapped provider which collects tx data 10 | */ 11 | export class GasReporterProvider extends ProviderWrapper { 12 | public isInitialized = false; 13 | private _executionContext: GasReporterExecutionContext | undefined; 14 | 15 | constructor(provider: EIP1193Provider) { 16 | super(provider); 17 | } 18 | 19 | /** 20 | * extendProvider doesn't expose the environment but that's where we store data 21 | * and context stuff while bookending the start & finish of other tasks 22 | * @param {GasReporterExecutionContext} context 23 | */ 24 | public initializeGasReporterProvider(context: GasReporterExecutionContext) { 25 | this.isInitialized = true; 26 | this._executionContext = context; 27 | } 28 | 29 | /** 30 | * Handles calls for TruffleV5 31 | * @param {RequestArguments} args 32 | * @returns {Promise} 33 | */ 34 | private async _handleTruffleV5(args: RequestArguments): Promise { 35 | const receipt: any = await this._wrappedProvider.request(args); 36 | 37 | if (receipt?.status && receipt?.transactionHash) { 38 | const tx = await this._wrappedProvider.request({ 39 | method: "eth_getTransactionByHash", 40 | params: [receipt.transactionHash], 41 | }); 42 | await this._executionContext!.collector?.collectTransaction(tx as JsonRpcTx, receipt); 43 | } 44 | return receipt; 45 | } 46 | 47 | /** 48 | * Handles calls for Ethers (`eth_getTransactionReceipt`). (Ethers calls this when tx is mined) 49 | * @param {RequestArguments} args 50 | * @returns {Promise} 51 | */ 52 | private async _handleEthers(args: RequestArguments): Promise { 53 | const receipt: any = await this._wrappedProvider.request({ 54 | method: "eth_getTransactionReceipt", 55 | params: args.params, 56 | }); 57 | const tx = await this._wrappedProvider.request(args); 58 | if (receipt?.status) { 59 | await this._executionContext!.collector?.collectTransaction(tx as JsonRpcTx, receipt); 60 | } 61 | return tx; 62 | } 63 | 64 | /** 65 | * Handles calls for Waffle (`eth_sendRawTransaction`) and Viem (`eth_sendTransaction`) 66 | * @param {RequestArguments} args 67 | * @returns {Promise} 68 | */ 69 | private async _handleViemOrWaffle(args: RequestArguments): Promise { 70 | const txHash = await this._wrappedProvider.request(args); 71 | 72 | if (typeof txHash === "string") { 73 | const tx = await this._wrappedProvider.request({ 74 | method: "eth_getTransactionByHash", 75 | params: [txHash], 76 | }); 77 | const receipt: any = await this._wrappedProvider.request({ 78 | method: "eth_getTransactionReceipt", 79 | params: [txHash], 80 | }); 81 | 82 | if (receipt?.status) { 83 | await this._executionContext!.collector?.collectTransaction(tx as JsonRpcTx, receipt); 84 | } 85 | } 86 | return txHash; 87 | } 88 | 89 | /** 90 | * Handles `eth_call` (for pure and view fns) 91 | * @param {RequestArguments} args 92 | * @returns {Promise} 93 | */ 94 | private async _handleEthCall(args: RequestArguments): Promise { 95 | let gas; 96 | if (this._canEstimate(args)){ 97 | try { 98 | gas = await this._wrappedProvider.request({ 99 | method: "eth_estimateGas", 100 | params: args.params, 101 | }); 102 | 103 | // Converting inside try block b/c not sure what edge case 104 | // responses are for all providers 105 | gas = hexToDecimal(gas as string); 106 | } catch (err) { 107 | gas = null; 108 | } 109 | 110 | if (gas) { 111 | await this._executionContext!.collector?.collectCall( 112 | args as ValidatedRequestArguments, 113 | gas 114 | ); 115 | } 116 | } 117 | return this._wrappedProvider.request(args); 118 | } 119 | 120 | /** 121 | * Dispatch table for all the call routes required for different providers and call types 122 | * @param {RequestArguments} args 123 | * @returns {Promise} 124 | */ 125 | public async request(args: RequestArguments): Promise { 126 | if (!this.isInitialized) { 127 | return this._wrappedProvider.request(args); 128 | } 129 | 130 | switch(args.method) { 131 | case "eth_call": return this._handleEthCall(args); 132 | case "eth_getTransactionReceipt": return this._handleTruffleV5(args); 133 | case "eth_getTransactionByHash": return this._handleEthers(args); 134 | case "eth_sendRawTransaction": return this._handleViemOrWaffle(args); 135 | case "eth_sendTransaction": return (this._executionContext!.usingViem) 136 | ? this._handleViemOrWaffle(args) 137 | : this._wrappedProvider.request(args); 138 | default: return this._wrappedProvider.request(args); 139 | } 140 | } 141 | 142 | /** 143 | * Used by `eth_call` to check that we're tracking calls and that the call being made is 144 | * not disallowed by a Resolver. Resolvers need to make private calls to establish proxied 145 | * contract identities - if we don't filter them here we get stuck in an infinite loop. 146 | * @param args 147 | * @returns 148 | */ 149 | private _canEstimate(args: RequestArguments) { 150 | if (!this._executionContext?.usingCall) return false; 151 | 152 | if ( 153 | Array.isArray(args.params) && 154 | args.params.length >=1 && 155 | typeof(args.params[0].data) === "string" 156 | ) { 157 | const sig = args.params[0].data.slice(2,10); 158 | for (const method of this._executionContext!.methodIgnoreList!){ 159 | if (method === sig) return false; 160 | } 161 | } 162 | // Only filtering the ignore list here, not any other issues 163 | return true; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/lib/render/index.ts: -------------------------------------------------------------------------------- 1 | import type { GasData } from "../gasData"; 2 | import { writeFileSync } from "fs"; 3 | import path from "path"; 4 | 5 | import { HardhatRuntimeEnvironment as HRE } from "hardhat/types"; 6 | import { 7 | TABLE_NAME_LEGACY, 8 | TABLE_NAME_MARKDOWN, 9 | TABLE_NAME_TERMINAL, 10 | CACHE_FILE_NAME 11 | } from "../../constants"; 12 | import { getSolcInfo } from "../../utils/sources"; 13 | import { warnReportFormat } from "../../utils/ui"; 14 | import { GasReporterOptions } from "../../types"; 15 | import { generateTerminalTextTable } from "./terminal"; 16 | import { generateLegacyTextTable } from "./legacy"; 17 | import { generateMarkdownTable} from "./markdown"; 18 | import { generateJSONData, loadJSONCache } from "./json"; 19 | 20 | 21 | /** 22 | * Table format selector 23 | * @param {HardhatRuntimeEnvironment} hre 24 | * @param {GasData} data 25 | * @param {GasReporterOptions} options 26 | * @param {string} toolchain 27 | * @returns {string} table 28 | */ 29 | export function getTableForFormat( 30 | hre: HRE, 31 | data: GasData, 32 | options: GasReporterOptions, 33 | toolchain="hardhat" 34 | ): string { 35 | switch (options.reportFormat) { 36 | case TABLE_NAME_LEGACY: return generateLegacyTextTable(hre, data, options); 37 | case TABLE_NAME_TERMINAL: return generateTerminalTextTable(hre, data, options, toolchain); 38 | case TABLE_NAME_MARKDOWN: return generateMarkdownTable(hre, data, options, toolchain); 39 | default: warnReportFormat(options.reportFormat); return ""; 40 | } 41 | } 42 | 43 | /** 44 | * Manages table rendering and file saving 45 | * @param {HardhatRuntimeEnvironment} hre 46 | * @param {GasReporterOptions} options 47 | * @param {string[]} warnings 48 | * @param {string} toolchain 49 | */ 50 | export function render( 51 | hre: HRE, 52 | options: GasReporterOptions, 53 | warnings: string[], 54 | toolchain="hardhat" 55 | ) { 56 | const data = hre.__hhgrec.collector!.data; 57 | options.blockGasLimit = hre.__hhgrec.blockGasLimit; 58 | options.solcInfo = getSolcInfo(hre.config.solidity.compilers[0]); 59 | 60 | if (options.trackGasDeltas) { 61 | options.cachePath = options.cachePath || path.resolve( 62 | hre.config.paths.cache, 63 | CACHE_FILE_NAME 64 | ); 65 | 66 | try { 67 | const previousData = loadJSONCache(options); 68 | data.addDeltas(previousData.data!); 69 | } catch {}; 70 | } 71 | 72 | 73 | // Get table 74 | let table = getTableForFormat(hre, data, options, toolchain); 75 | 76 | // --------------------------------------------------------------------------------------------- 77 | // RST / ReadTheDocs / Sphinx output 78 | // --------------------------------------------------------------------------------------------- 79 | let rstOutput = ""; 80 | if (options.rst) { 81 | rstOutput += `${options.rstTitle!}\n`; 82 | rstOutput += `${"=".repeat(options.rstTitle!.length)}\n\n`; 83 | rstOutput += `.. code-block:: shell\n\n`; 84 | } 85 | 86 | table = rstOutput + table; 87 | 88 | // --------------------------------------------------------------------------------------------- 89 | // Print 90 | // --------------------------------------------------------------------------------------------- 91 | if (options.outputFile) { 92 | writeFileSync(options.outputFile, table); 93 | 94 | // Regenerate the table with full color if also logging to console 95 | if (options.forceTerminalOutput){ 96 | const originalOutputFile = options.outputFile; 97 | const originalNoColors = options.noColors; 98 | const originalReportFormat = options.reportFormat; 99 | 100 | options.outputFile = undefined; 101 | options.noColors = false; 102 | 103 | options.reportFormat = (options.forceTerminalOutputFormat) 104 | ? options.forceTerminalOutputFormat 105 | : options.reportFormat; 106 | 107 | table = getTableForFormat(hre, data, options); 108 | console.log(table); 109 | 110 | // Reset the options, since they might be read in JSON below here 111 | options.outputFile = originalOutputFile; 112 | options.noColors = originalNoColors; 113 | options.reportFormat = originalReportFormat; 114 | } 115 | } else if (!options.suppressTerminalOutput) { 116 | console.log(table); 117 | } 118 | 119 | if (options.outputJSON || process.env.CI) { 120 | generateJSONData(data, options, toolchain); 121 | } 122 | 123 | if (options.trackGasDeltas) { 124 | options.outputJSONFile = options.cachePath!; 125 | generateJSONData(data, options, toolchain); 126 | } 127 | 128 | // Write warnings 129 | for (const warning of warnings) console.log(warning); 130 | } 131 | -------------------------------------------------------------------------------- /src/lib/render/json.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync } from "fs"; 2 | import { GasData } from "../gasData"; 3 | import { GasReporterOptions, GasReporterOutput } from "../../types"; 4 | 5 | /** 6 | * Writes accumulated data and the current options to gasReporterOutput.json so it 7 | * can be consumed by other tools (CI etc...) 8 | * @param {GasData} data 9 | * @param {GasReporterOptions} options 10 | * @param {string} toolchain 11 | */ 12 | export function generateJSONData( 13 | data: GasData, 14 | options: GasReporterOptions, 15 | toolchain="hardhat" 16 | ) { 17 | const pkg = require("../../../package.json"); 18 | _sanitizeGasData(data, options); 19 | 20 | const output: GasReporterOutput = { 21 | namespace: "HardhatGasReporter", 22 | toolchain, 23 | version: pkg.version, 24 | options, 25 | data 26 | }; 27 | 28 | writeFileSync(options.outputJSONFile!, JSON.stringify(output, null, ' ')); 29 | } 30 | 31 | /** 32 | * Reads previous acccumulated data and options from cache so it can be used to calculate deltas 33 | * @param {GasReporterOptions} options 34 | * @returns {GasReporterOptions} previous data and options 35 | */ 36 | export function loadJSONCache( 37 | options: GasReporterOptions 38 | ): GasReporterOutput { 39 | return JSON.parse(readFileSync(options.cachePath!).toString()); 40 | } 41 | 42 | /** 43 | * Removes extraneous data and attached methods 44 | * @param {GasData} data 45 | * @param {GasReporterOptions} options 46 | */ 47 | function _sanitizeGasData(data: GasData, options: GasReporterOptions) { 48 | delete options.proxyResolver; 49 | 50 | delete (data as any).addressCache; 51 | delete (data as any).codeHashMap; 52 | delete data.provider; 53 | 54 | if (!options.includeBytecodeInJSON) { 55 | data.deployments.forEach(deployment => { 56 | delete (deployment as any).bytecode; 57 | delete (deployment as any).deployedBytecode; 58 | }) 59 | } 60 | 61 | if (options.coinmarketcap){ 62 | options.coinmarketcap = "[REDACTED]"; 63 | } 64 | 65 | if (options.etherscan) { 66 | options.etherscan = "[REDACTED]"; 67 | } 68 | 69 | // Options deprecated in 2.3.0 70 | if (options.L1Etherscan){ 71 | options.L1Etherscan = "[REDACTED]"; 72 | } 73 | 74 | if (options.L2Etherscan){ 75 | options.L2Etherscan = "[REDACTED]"; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/render/legacy.ts: -------------------------------------------------------------------------------- 1 | import chalk, {Chalk} from "chalk"; 2 | import _ from "lodash"; 3 | import Table, { HorizontalTableRow } from "cli-table3"; 4 | import { commify } from "@ethersproject/units"; 5 | 6 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 7 | import { GasReporterOptions, MethodDataItem } from "../../types"; 8 | 9 | import { GasData } from "../gasData"; 10 | 11 | /** 12 | * LEGACY ONLY 13 | * IGNORE THIS FORMAT WHEN ADDING INFO TO TABLES (UNLESS BUG FIXING) 14 | */ 15 | 16 | /** 17 | * Generates a gas statistics text table formatted for terminal or file. 18 | * Based on Alan Lu's (github.com/cag) stats for Gnosis 19 | * @param {HardhatRuntimeEnvironment} hre 20 | * @param {GasData} data 21 | * @param {GasReporterOptions} options 22 | */ 23 | export function generateLegacyTextTable( 24 | hre: HardhatRuntimeEnvironment, 25 | data: GasData, 26 | options: GasReporterOptions 27 | ): string { 28 | 29 | let optionalColor: Chalk; 30 | 31 | if (options.noColors || options.outputFile !== undefined) { 32 | chalk.level = 0; 33 | } else { 34 | chalk.level = 1; 35 | } 36 | 37 | if (options.darkMode) { 38 | optionalColor = chalk.cyan; 39 | } else { 40 | optionalColor = chalk.grey; 41 | } 42 | 43 | // --------------------------------------------------------------------------------------------- 44 | // Assemble section: methods 45 | // --------------------------------------------------------------------------------------------- 46 | const methodRows: any[] = []; 47 | 48 | _.forEach(data.methods, (method: MethodDataItem) => { 49 | if (!method) return; 50 | 51 | const stats: any = {}; 52 | 53 | if (method.gasData.length > 0) { 54 | stats.executionGasAverage = commify(method.executionGasAverage!); 55 | stats.cost = (method.cost === undefined) ? chalk.grey("-") : method.cost; 56 | } else { 57 | stats.executionGasAverage = chalk.grey("-"); 58 | stats.cost = chalk.grey("-"); 59 | } 60 | 61 | if (method.min && method.max) { 62 | const uniform = (method.min === method.max); 63 | stats.min = uniform ? chalk.grey("-") : chalk.cyan(commify(method.min!)); 64 | stats.max = uniform ? chalk.grey("-") : chalk.red(commify(method.max!)); 65 | } 66 | 67 | stats.numberOfCalls = optionalColor(method.numberOfCalls.toString()); 68 | 69 | const fnName = options.showMethodSig ? method.fnSig : method.method; 70 | 71 | if (options.showUncalledMethods || method.numberOfCalls > 0) { 72 | const section: any = []; 73 | section.push(optionalColor.bold(method.contract)); 74 | section.push(fnName); 75 | section.push({ hAlign: "right", content: stats.min }); 76 | section.push({ hAlign: "right", content: stats.max }); 77 | section.push({ hAlign: "right", content: stats.executionGasAverage }); 78 | section.push({ hAlign: "right", content: stats.numberOfCalls }); 79 | section.push({ 80 | hAlign: "right", 81 | content: chalk.green(stats.cost.toString()) 82 | }); 83 | 84 | methodRows.push(section); 85 | } 86 | }); 87 | 88 | // --------------------------------------------------------------------------------------------- 89 | // Assemble section: deployments 90 | // --------------------------------------------------------------------------------------------- 91 | const deployRows: any = []; 92 | // Alphabetize contract names 93 | data.deployments.sort((a, b) => a.name.localeCompare(b.name)); 94 | 95 | data.deployments.forEach(deployment => { 96 | const stats: any = {}; 97 | if (deployment.gasData.length === 0) return; 98 | 99 | stats.cost = (deployment.cost === undefined) ? chalk.grey("-") : deployment.cost; 100 | 101 | if (deployment.min && deployment.max) { 102 | const uniform = deployment.min === deployment.max; 103 | stats.min = uniform ? chalk.grey("-") : chalk.cyan(commify(deployment.min!)); 104 | stats.max = uniform ? chalk.grey("-") : chalk.red(commify(deployment.max!)); 105 | } 106 | 107 | const section: any = []; 108 | section.push({ hAlign: "left", colSpan: 2, content: deployment.name }); 109 | section.push({ hAlign: "right", content: stats.min }); 110 | section.push({ hAlign: "right", content: stats.max }); 111 | section.push({ hAlign: "right", content: commify(deployment.executionGasAverage!) }); 112 | section.push({ 113 | hAlign: "right", 114 | content: optionalColor(`${deployment.percent!} %`) 115 | }); 116 | section.push({ 117 | hAlign: "right", 118 | content: chalk.green(stats.cost.toString()) 119 | }); 120 | 121 | deployRows.push(section); 122 | }); 123 | 124 | // --------------------------------------------------------------------------------------------- 125 | // Assemble section: headers 126 | // --------------------------------------------------------------------------------------------- 127 | 128 | // Configure indentation for RTD 129 | const leftPad = options.rst ? " " : ""; 130 | 131 | // Format table 132 | const table = new Table({ 133 | style: { head: [], border: [], "padding-left": 2, "padding-right": 2 }, 134 | chars: { 135 | mid: "·", 136 | "top-mid": "|", 137 | "left-mid": `${leftPad}·`, 138 | "mid-mid": "|", 139 | "right-mid": "·", 140 | left: `${leftPad}|`, 141 | "top-left": `${leftPad}·`, 142 | "top-right": "·", 143 | "bottom-left": `${leftPad}·`, 144 | "bottom-right": "·", 145 | middle: "·", 146 | top: "-", 147 | bottom: "-", 148 | "bottom-mid": "|" 149 | } 150 | }); 151 | 152 | // Format and load methods metrics 153 | const title = [ 154 | { 155 | hAlign: "center", 156 | colSpan: 2, 157 | content: optionalColor.bold(`Solc version: ${options.solcInfo.version}`) 158 | }, 159 | { 160 | hAlign: "center", 161 | colSpan: 2, 162 | content: optionalColor.bold(`Optimizer enabled: ${options.solcInfo.optimizer}`) 163 | }, 164 | { 165 | hAlign: "center", 166 | colSpan: 1, 167 | content: optionalColor.bold(`Runs: ${options.solcInfo.runs}`) 168 | }, 169 | { 170 | hAlign: "center", 171 | colSpan: 2, 172 | content: optionalColor.bold(`Block limit: ${commify(options.blockGasLimit!)} gas`) 173 | } 174 | ]; 175 | 176 | let methodSubtitle; 177 | if (options.tokenPrice && options.gasPrice) { 178 | const gwei = options.gasPrice; 179 | const rate = parseFloat(options.tokenPrice.toString()).toFixed(2); 180 | const currency = `${options.currency!.toLowerCase()}`; 181 | const token = `${options.token!.toLowerCase()}`; 182 | 183 | methodSubtitle = [ 184 | { hAlign: "left", colSpan: 2, content: chalk.green.bold("Methods") }, 185 | { 186 | hAlign: "center", 187 | colSpan: 3, 188 | content: chalk.cyan.bold(`${gwei} gwei/gas`) 189 | }, 190 | { 191 | hAlign: "center", 192 | colSpan: 2, 193 | content: chalk.red.bold(`${rate} ${currency}/${token}`) 194 | } 195 | ]; 196 | } else { 197 | methodSubtitle = [ 198 | { hAlign: "left", colSpan: 7, content: chalk.green.bold("Methods") } 199 | ]; 200 | } 201 | 202 | const header = [ 203 | chalk.bold("Contract"), 204 | chalk.bold("Method"), 205 | chalk.green("Min"), 206 | chalk.green("Max"), 207 | chalk.green("Avg"), 208 | chalk.bold("# calls"), 209 | chalk.bold(`${options.currency!.toLowerCase()} (avg)`) 210 | ]; 211 | 212 | // --------------------------------------------------------------------------------------------- 213 | // Final assembly 214 | // --------------------------------------------------------------------------------------------- 215 | table.push(title as HorizontalTableRow); 216 | table.push(methodSubtitle as HorizontalTableRow); 217 | table.push(header); 218 | 219 | methodRows.sort((a, b) => { 220 | const contractName = a[0].localeCompare(b[0]); 221 | const methodName = a[1].localeCompare(b[1]); 222 | return contractName || methodName; 223 | }); 224 | 225 | methodRows.forEach(row => table.push(row)); 226 | 227 | if (deployRows.length) { 228 | const deploymentsSubtitle = [ 229 | { 230 | hAlign: "left", 231 | colSpan: 2, 232 | content: chalk.green.bold("Deployments") 233 | }, 234 | { hAlign: "right", colSpan: 3, content: "" }, 235 | { hAlign: "left", colSpan: 1, content: chalk.bold(`% of limit`) } 236 | ]; 237 | table.push(deploymentsSubtitle as HorizontalTableRow); 238 | deployRows.forEach((row: any) => table.push(row)); 239 | } 240 | 241 | return table.toString(); 242 | } 243 | 244 | -------------------------------------------------------------------------------- /src/lib/resolvers/etherrouter.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from "@ethersproject/abi"; 2 | import { hexStripZeros } from "@ethersproject/bytes"; 3 | 4 | import { getHashedFunctionSignature } from "../../utils/sources"; 5 | import { CustomGasReporterResolver, JsonRpcTx } from "../../types"; 6 | import { Resolver } from "./index"; 7 | 8 | 9 | const ABI = ["function resolver()", "function lookup(bytes4 sig)"]; 10 | 11 | /** 12 | * Example of a class that resolves the contract names of method calls routed through 13 | * a simple proxy (EtherRouter-style) contract. At runtime, the function below will be bound to 14 | * the `this` property of plugin's Resolver class and inherit its resources which include: 15 | * 16 | * > helpers to match methods to contracts (e.g all public methods on the Resolver & GasData classes) 17 | * > the HardhatRuntimeEnvironment (so you can access all env extensions and the network provider.) 18 | * 19 | * The method receives a JSONRPC formatted transaction object representing a tx 20 | * the reporter could not deterministically associate with any contract. It relies on your 21 | * knowledge of a proxy contract's API to derive the correct contract name. 22 | * 23 | * Returns contract name matching the resolved address. 24 | * @param {Object} transaction JSONRPC formatted transaction 25 | * @return {String} contract name 26 | */ 27 | export class EtherRouterResolver implements CustomGasReporterResolver { 28 | /** 29 | * Because the gas reporter tracks `eth_calls` made with the provider, the class 30 | * needs to declare the function signatures it uses to retrieve the identity of proxied 31 | * targets on-chain. If these aren't filtered by the GasReporterProvider it gets 32 | * trapped in an infinite loop. 33 | */ 34 | public ignore(): string[] { 35 | const functions = new Interface(ABI).functions 36 | const signatures = Object.keys(functions) 37 | return [ 38 | getHashedFunctionSignature(signatures[0]), 39 | getHashedFunctionSignature(signatures[1]) 40 | ]; 41 | } 42 | 43 | public async resolve(this: Resolver, transaction: JsonRpcTx): Promise { 44 | let contractAddress; 45 | let contractName; 46 | 47 | try { 48 | const iface = new Interface(ABI); 49 | 50 | // The tx passed to this method had input data which didn't map to any methods on 51 | // the contract it was sent to. We know the tx's `to` address might point to 52 | // a router contract which forward calls so we grab the method signature and ask 53 | // the router if it knows who the intended recipient is. 54 | const signature = transaction.input.slice(0, 10); 55 | 56 | // The router has a public state variable called `resolver()` which stores the 57 | // address of a contract which maps method signatures to their parent contracts. 58 | const resolverAddress = await this.hre.network.provider.send("eth_call", [{ 59 | to: transaction.to!, 60 | data: iface.encodeFunctionData("resolver()", []) 61 | }]); 62 | 63 | // Now we'll call the EtherRouterResolver contract's `lookup(sig)` method to get 64 | // the address of the contract our tx was actually getting forwarded to. 65 | contractAddress = await this.hre.network.provider.send("eth_call",[ 66 | { 67 | to: hexStripZeros(resolverAddress), 68 | data: iface.encodeFunctionData("lookup(bytes4)", [signature]) 69 | } 70 | ]); 71 | 72 | contractAddress = hexStripZeros(contractAddress); 73 | 74 | // Don't forget this is all speculative... 75 | } catch (err) { 76 | this.unresolvedCalls++; 77 | return null; 78 | } 79 | 80 | // With the correct address, we can use the reporter's Resolver class methods 81 | // `data.getNameByAddress` and/or `resolveByDeployedBytecode` to derive 82 | // the target contract's name. 83 | if (contractAddress && contractAddress !== "0x") { 84 | contractName = await this.data.getNameByAddress(contractAddress); 85 | 86 | // Try to resolve by deployedBytecode 87 | if (contractName) return contractName; 88 | else return this.resolveByDeployedBytecode(contractAddress); 89 | } 90 | 91 | return null; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import type {GasData} from "../gasData"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { CustomGasReporterResolver, GasReporterOptions, JsonRpcTx } from "../../types"; 4 | 5 | import { OZResolver } from "./oz"; 6 | 7 | export class Resolver { 8 | public unresolvedCalls: number; 9 | public data: GasData; 10 | public hre: HardhatRuntimeEnvironment; 11 | public resolveByProxy: Function; 12 | 13 | constructor(hre: HardhatRuntimeEnvironment, options: GasReporterOptions, data: GasData) { 14 | this.unresolvedCalls = 0; 15 | this.data = data; 16 | this.hre = hre; 17 | 18 | if (options.proxyResolver !== undefined) { 19 | this.resolveByProxy = (options.proxyResolver as CustomGasReporterResolver).resolve.bind(this); 20 | } else if (hre.__hhgrec.usingOZ) { 21 | this.resolveByProxy = new OZResolver().resolve.bind(this); 22 | } else { 23 | this.resolveByProxy = this.resolveByMethodSignature; 24 | } 25 | } 26 | 27 | /** 28 | * Searches all known contracts for the method signature and returns the first 29 | * found (if any). Undefined if none 30 | * @param {Object} tx result of web3.eth_getTransaction 31 | * @return {String} contract name 32 | */ 33 | public resolveByMethodSignature(tx: JsonRpcTx): string | null { 34 | const signature = tx.input.slice(2, 10); 35 | const matches = this.data.getAllContractsWithMethod(signature); 36 | 37 | if (matches.length >= 1) return matches[0].contract; 38 | return null; 39 | } 40 | 41 | /** 42 | * Tries to match bytecode deployed at address to deployedBytecode listed 43 | * in artifacts. If found, adds this to the code-hash name mapping and 44 | * returns name. 45 | * @param {String} address contract address 46 | * @return {String} contract name 47 | */ 48 | public async resolveByDeployedBytecode(address: string | null): Promise { 49 | if (!address) return null; 50 | 51 | const code = await this.hre.network.provider.send("eth_getCode", [address, "latest"]); 52 | const match = this.data.getContractByDeployedBytecode(code); 53 | 54 | if (match !== null) { 55 | await this.data.trackNameByAddress(match.name, address); 56 | return match.name; 57 | } 58 | return null; 59 | } 60 | 61 | /** 62 | * Helper for CustomResolvers which checks the existing contract address cache before 63 | * trying to resolve by deployed bytecode 64 | * @param contractAddress 65 | * @returns 66 | */ 67 | public async resolveViaCache(contractAddress: string): Promise { 68 | if (contractAddress && contractAddress !== "0x") { 69 | const contractName = await this.data.getNameByAddress(contractAddress); 70 | 71 | if (contractName) return contractName; 72 | 73 | // Try to resolve by deployedBytecode 74 | return this.resolveByDeployedBytecode(contractAddress); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/resolvers/oz.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcTx, CustomGasReporterResolver } from "../../types"; 2 | import { Resolver } from "./index"; 3 | 4 | 5 | /** 6 | * Custom resolver for OpenZeppelin's upgrades and defender plugins. There are two 7 | * types of upgrade for both systems, `erc1967` and `beacon`. They are queried in a 8 | * series for the missing contract's implementation address. This resolver gets attached 9 | * by default when `upgrades` or `defender` are present on the `hre` (as long as user 10 | * hasn't defined their own). 11 | * 12 | * Returns contract name matching the resolved address. 13 | * @param {Resolver} `this` 14 | * @param {JsonRpcTx} transaction JSONRPC formatted transaction 15 | * @return {Promise} contract name 16 | */ 17 | export class OZResolver implements CustomGasReporterResolver { 18 | 19 | /** 20 | * OZ's `getImplementationAddress` and `getBeaconAddress` query a storage slot in the proxy 21 | * or beacon using `eth_getStorageAt`. There's no conflict with `eth_call` 22 | * https://github.com/OpenZeppelin/openzeppelin-upgrades/... 23 | * ...blob/5785b3f6b788c4c905e277486a7c1b45fd0ad45b/packages/core/src/provider.ts#L75C40-L75C56 24 | * @returns [] 25 | */ 26 | public ignore(): string[] { 27 | return []; 28 | } 29 | 30 | public async resolve(this: Resolver, transaction: JsonRpcTx): Promise { 31 | let contractAddress; 32 | 33 | try { 34 | contractAddress = await (this.hre as any).upgrades.erc1967.getImplementationAddress(transaction.to!); 35 | const contractName = await this.resolveViaCache(contractAddress); 36 | if (contractName) return contractName; 37 | } catch(err) {} 38 | 39 | try { 40 | const beaconAddress = await (this.hre as any).upgrades.erc1967.getBeaconAddress(transaction.to!); 41 | contractAddress = await (this.hre as any).upgrades.beacon.getImplementationAddress(beaconAddress); 42 | const contractName = await this.resolveViaCache(contractAddress); 43 | if (contractName) return contractName; 44 | } catch(err) {} 45 | 46 | try { 47 | contractAddress = await (this.hre as any).defender.erc1967.getImplementationAddress(transaction.to!); 48 | const contractName = await this.resolveViaCache(contractAddress); 49 | if (contractName) return contractName; 50 | } catch(err) {} 51 | 52 | try { 53 | const beaconAddress = await (this.hre as any).defender.erc1967.getBeaconAddress(transaction.to!); 54 | contractAddress = await (this.hre as any).defender.beacon.getImplementationAddress(beaconAddress); 55 | const contractName = await this.resolveViaCache(contractAddress); 56 | if (contractName) return contractName; 57 | } catch(err) {} 58 | 59 | this.unresolvedCalls++; 60 | return null; 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/lib/validators/foundry.ts: -------------------------------------------------------------------------------- 1 | /* placeholder */ 2 | -------------------------------------------------------------------------------- /src/lib/validators/hardhat.ts: -------------------------------------------------------------------------------- 1 | import { Validator, ValidatorResult } from 'jsonschema'; 2 | import { HardhatPluginError } from 'hardhat/plugins'; 3 | import { EOL } from 'os'; 4 | import { Deployment, GasReporterOutput, MethodDataItem } from '../../types'; 5 | 6 | const ReporterOutputSchema = { 7 | id: "hardhat.hhgr.output.json", 8 | type: "object", 9 | properties: { 10 | namespace: { type: "string" }, 11 | toolchain: { type: "string" }, 12 | version: { type: "string"}, 13 | options: { type: "object"}, 14 | data: { 15 | type: "object", 16 | properties: { 17 | "methods": { type: "object"}, 18 | "deployments": { type: "array", "items": { "type": "object"} } 19 | }, 20 | required: ["methods", "deployments"] 21 | } 22 | }, 23 | required: ["toolchain", "data", "options"] 24 | }; 25 | 26 | const MethodDataSchema = { 27 | id: "hardhat.hhgr.methods.json", 28 | type: "object", 29 | properties: { 30 | callData: { type: "array", "items": { "type": "number"}, "required": true }, 31 | gasData: { type: "array", "items": { "type": "number"}, "required": true }, 32 | numberOfCalls: { type: "number", "required": true}, 33 | } 34 | }; 35 | 36 | const DeploymentDataSchema = { 37 | id: "hardhat.hhgr.deployments.json", 38 | type: "object", 39 | properties: { 40 | name: {type: "string", "required": true}, 41 | callData: { type: "array", "items": { "type": "number"}, "required": true }, 42 | gasData: { type: "array", "items": { "type": "number"}, "required": true }, 43 | } 44 | } 45 | 46 | export class HardhatGasReporterOutputValidator { 47 | public validator: Validator; 48 | 49 | constructor(){ 50 | this.validator = new Validator(); 51 | this.validator.addSchema(ReporterOutputSchema); 52 | this.validator.addSchema(MethodDataSchema); 53 | this.validator.addSchema(DeploymentDataSchema); 54 | } 55 | 56 | public validateOutputObject(output: GasReporterOutput, sourceFilePath: string){ 57 | const result = this.validator.validate(output, ReporterOutputSchema); 58 | this._checkResult(result, sourceFilePath); 59 | return true; 60 | } 61 | 62 | public validateMethodDataItem(item: MethodDataItem, sourceFilePath: string){ 63 | const result = this.validator.validate(item, MethodDataSchema); 64 | this._checkResult(result, sourceFilePath); 65 | return true; 66 | } 67 | 68 | public validateDeploymentDataItem(deployment: Deployment, sourceFilePath: string){ 69 | const result = this.validator.validate(deployment, DeploymentDataSchema); 70 | this._checkResult(result, sourceFilePath); 71 | return true; 72 | } 73 | 74 | private _checkResult(result: ValidatorResult, sourceFilePath: string) { 75 | if (result.errors.length){ 76 | let errors = ""; 77 | for (const err of result.errors) { 78 | errors += err.stack.replace("instance.", "") + EOL; 79 | }; 80 | 81 | throw new HardhatPluginError( 82 | "hardhat-gas-reporter", 83 | `Unexpected JSON report format in ${sourceFilePath}. ` + 84 | `Reported JSON validation error was: ${ EOL 85 | }${errors}` 86 | ); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/task-names.ts: -------------------------------------------------------------------------------- 1 | export const TASK_GAS_REPORTER_MERGE_REPORTS = "hhgas:merge-reports"; 2 | export const TASK_GAS_REPORTER_MERGE = "hhgas:merge"; 3 | export const TASK_GAS_REPORTER_START = "hhgas:start"; 4 | export const TASK_GAS_REPORTER_STOP = "hhgas:stop"; 5 | 6 | export const TASK_GAS_REPORTER_MERGE_REPORTS_LEGACY = "gas-reporter:merge-reports"; 7 | export const TASK_GAS_REPORTER_MERGE_LEGACY = "gas-reporter:merge"; 8 | -------------------------------------------------------------------------------- /src/tasks/mergeReports.ts: -------------------------------------------------------------------------------- 1 | import { HardhatPluginError } from "hardhat/plugins"; 2 | import { HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types"; 3 | import { TASK_GAS_REPORTER_MERGE_REPORTS } from "../task-names"; 4 | import { GasReporterOutput } from "../types"; 5 | 6 | /** 7 | * Tries to merge several gas reporter output objects into one. 8 | */ 9 | export async function mergeReports( 10 | reports: GasReporterOutput[], 11 | inputFiles: string[] 12 | ): Promise { 13 | const result: any = { 14 | namespace: null, 15 | toolchain: null, 16 | options: null, 17 | data: { 18 | methods: {}, 19 | deployments: [], 20 | blockLimit: null, 21 | }, 22 | }; 23 | 24 | const { HardhatGasReporterOutputValidator } = await import("../lib/validators/hardhat"); 25 | 26 | for (const [index, report] of reports.entries()) { 27 | const Validator = new HardhatGasReporterOutputValidator(); 28 | Validator.validateOutputObject(report, inputFiles[index]); 29 | 30 | if (result.options === null) result.options = report.options; 31 | if (result.namespace === null) result.namespace = report.namespace; 32 | if (result.toolchain === null) result.toolchain = report.toolchain; 33 | 34 | // Merge data.methods objects 35 | Object.entries(report.data!.methods).forEach(([key, value]) => { 36 | Validator.validateMethodDataItem(value, inputFiles[index]); 37 | if (result.data.methods[key] === undefined) { 38 | result.data.methods[key] = value; 39 | return; 40 | } 41 | 42 | result.data.methods[key].gasData = [ 43 | ...result.data!.methods[key].gasData, 44 | ...report.data!.methods[key].gasData, 45 | ].sort((a, b) => a - b); 46 | 47 | result.data.methods[key].callData = [ 48 | ...result.data!.methods[key].callData, 49 | ...report.data!.methods[key].callData, 50 | ].sort((a, b) => a - b); 51 | 52 | result.data.methods[key].numberOfCalls += 53 | report.data!.methods[key].numberOfCalls; 54 | }); 55 | 56 | // Merge data.deployments objects 57 | report.data!.deployments.forEach((deployment) => { 58 | const current = result.data.deployments.find( 59 | (d: any) => d.name === deployment.name 60 | ); 61 | 62 | if (current !== undefined) { 63 | current.gasData = [...current.gasData, ...deployment.gasData].sort( 64 | (a, b) => a - b 65 | ); 66 | current.callData = [...current.callData, ...deployment.callData].sort( 67 | (a, b) => a - b 68 | ); 69 | } else { 70 | result.data.deployments.push(deployment); 71 | } 72 | }); 73 | } 74 | 75 | return result; 76 | } 77 | 78 | /** 79 | * Task for merging multiple gasReporterOutput.json files generated by the plugin 80 | * This task is necessary when we want to generate different parts of the reports 81 | * parallelized on different jobs. 82 | */ 83 | export async function subtaskMergeReportsImplementation( 84 | { inputFiles }: { inputFiles: string[] } 85 | ): Promise { 86 | const fs = await import("fs"); 87 | 88 | const reports = inputFiles.map((input) => JSON.parse(fs.readFileSync(input, "utf-8"))); 89 | return mergeReports(reports, inputFiles); 90 | }; 91 | 92 | 93 | export async function taskMergeImplementation( 94 | taskArguments: TaskArguments, 95 | hre: HardhatRuntimeEnvironment 96 | ): Promise { 97 | const path = await import("path"); 98 | const { globSync } = await import("glob"); 99 | const { uniq } = await import("lodash"); 100 | const { reportMerge } = await import("../utils/ui"); 101 | const { GasData } = await import("../lib/gasData"); 102 | const { setGasAndPriceRates } = await import("../utils/prices"); 103 | const { generateJSONData } = await import("../lib/render/json"); 104 | 105 | 106 | const output = path.resolve(process.cwd(), taskArguments.output); 107 | 108 | // Parse input files and calculate glob patterns 109 | const taskArgs = uniq(taskArguments.input.map((input: string) => globSync(input)).flat()); 110 | const files = taskArgs.map((file) => path.resolve(file as string)) 111 | 112 | if (files.length === 0) { 113 | throw new HardhatPluginError( 114 | `hardhat-gas-reporter`, 115 | `No files found for the given input: ${taskArguments.input.join(" ")}` 116 | ); 117 | } 118 | 119 | reportMerge(files, output); 120 | 121 | const result = await hre.run(TASK_GAS_REPORTER_MERGE_REPORTS, { inputFiles: files }); 122 | const warnings = await setGasAndPriceRates(result.options); 123 | const data = new GasData(result.data.methods, result.data.deployments); 124 | await data.runAnalysis(hre, result.options); 125 | 126 | // Write warnings 127 | for (const warning of warnings) console.log(warning); 128 | 129 | // Write json 130 | result.options.outputJSONFile = output; 131 | generateJSONData(data, result.options); 132 | }; 133 | -------------------------------------------------------------------------------- /src/type-extensions.ts: -------------------------------------------------------------------------------- 1 | import { GasReporterOptions, GasReporterExecutionContext } from "./types"; 2 | 3 | /* Type Extensions */ 4 | declare module "hardhat/types/config" { 5 | interface HardhatConfig { 6 | gasReporter: Partial; 7 | } 8 | } 9 | 10 | declare module "hardhat/types/config" { 11 | interface HardhatUserConfig { 12 | gasReporter?: Partial; 13 | } 14 | } 15 | 16 | declare module "hardhat/types/runtime" { 17 | export interface HardhatRuntimeEnvironment { 18 | // eslint-disable-next-line @typescript-eslint/naming-convention 19 | __hhgrec: GasReporterExecutionContext 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/chains.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_API_KEY_ARGS, 3 | DEFAULT_GAS_PRICE_API_ARGS, 4 | DEFAULT_GET_BLOCK_API_ARGS, 5 | DEFAULT_BLOB_BASE_FEE_API_ARGS, 6 | DEFAULT_BASE_FEE_PER_BYTE_API_ARGS, 7 | } from "../constants" 8 | import { GasReporterOptions } from "../types" 9 | 10 | /** 11 | * Returns the CMC token symbol for a chain's native token. Uses chain configs below 12 | * and defers to user overrides 13 | * @param {GasReporterOptions} options 14 | * @returns 15 | */ 16 | export function getTokenForChain(options: GasReporterOptions): string { 17 | if (options.token) return options.token; 18 | 19 | // Gets caught and translated to warning 20 | if (!L1[options.L1!]) throw new Error(); 21 | 22 | return L1[options.L1!].token; 23 | } 24 | 25 | /** 26 | * Gets Etherscan gasPrice api call url for chain. Attaches L1 or L2 apikey if configured 27 | * @param {GasReporterOptions} options 28 | * @returns 29 | */ 30 | export function getGasPriceUrlForChain(options: GasReporterOptions): string { 31 | if (options.gasPriceApi) return options.gasPriceApi; 32 | 33 | const apiKey = (options.etherscan) ? `${DEFAULT_API_KEY_ARGS}${options.etherscan}` : ""; 34 | 35 | if (options.L2) { 36 | if (!L2[options.L2]) throw new Error; 37 | return `${L2[options.L2!].baseUrl}${DEFAULT_GAS_PRICE_API_ARGS}${apiKey}`; 38 | } 39 | 40 | if (!L1[options.L1!]) throw new Error(); 41 | return `${L1[options.L1!].baseUrl}${DEFAULT_GAS_PRICE_API_ARGS}${apiKey}`; 42 | } 43 | 44 | /** 45 | * Gets Etherscan getBlock api call url for chain. Attaches L1 apikey if configured 46 | * (THIS IS ALWAYS for L1 data-related fees in the context of L2 execution) 47 | * @param {GasReporterOptions} options 48 | * @returns 49 | */ 50 | export function getBlockUrlForChain(options: GasReporterOptions): string { 51 | if (!options.L2) return ""; 52 | if (options.getBlockApi) return options.getBlockApi; 53 | 54 | const apiKey = (options.etherscan) ? `${DEFAULT_API_KEY_ARGS}${options.etherscan}` : ""; 55 | 56 | if (!L1[options.L1!]) throw new Error(); 57 | 58 | return `${L1[options.L1!].baseUrl}${DEFAULT_GET_BLOCK_API_ARGS}${apiKey}`; 59 | } 60 | 61 | /** 62 | * Gets Etherscan eth_call api url to read OP Stack GasPriceOracle for blobBaseFee. 63 | * Attaches apikey if configured. (This fee fetched from L2 contract b/c its the only available place at 64 | * time of PR - eth_blobBaseFee hasn't been implemented in geth yet) 65 | * @param {GasReporterOptions} options 66 | * @returns 67 | */ 68 | export function getBlobBaseFeeUrlForChain(options: GasReporterOptions): string { 69 | if (!options.L2) return ""; 70 | if (options.blobBaseFeeApi) return options.blobBaseFeeApi; 71 | 72 | const apiKey = (options.etherscan) ? `${DEFAULT_API_KEY_ARGS}${options.etherscan}` : ""; 73 | 74 | return `${L2[options.L2!].baseUrl}${DEFAULT_BLOB_BASE_FEE_API_ARGS}${L2[options.L2!].gasPriceOracle}${apiKey}`; 75 | } 76 | 77 | /** 78 | * Gets Etherscan eth_call api url to read OP Stack GasPriceOracle for blobBaseFee. 79 | * Attaches apikey if configured. (This fee fetched from L2 contract b/c its the only available place at 80 | * time of PR - eth_blobBaseFee hasn't been implemented in geth yet) 81 | * @param {GasReporterOptions} options 82 | * @returns 83 | */ 84 | export function getBaseFeePerByteUrlForChain(options: GasReporterOptions): string { 85 | if (options.L2 !== "arbitrum") return ""; 86 | 87 | const apiKey = (options.etherscan) ? `${DEFAULT_API_KEY_ARGS}${options.etherscan}` : ""; 88 | 89 | return `${L2[options.L2!].baseUrl}${DEFAULT_BASE_FEE_PER_BYTE_API_ARGS}${apiKey}`; 90 | } 91 | 92 | /** 93 | * L1 & L2 chain configurations for fetching gas price and block fee data from Etherscan as well 94 | * as currency prices from Coinmarketcap 95 | */ 96 | export const L1 = { 97 | ethereum: { 98 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=1&", 99 | token: "ETH" 100 | }, 101 | polygon: { 102 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=137&", 103 | token: "POL" 104 | }, 105 | binance: { 106 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=56&", 107 | token: "BNB" 108 | }, 109 | fantom: { 110 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=250&", 111 | token: "FTM" 112 | }, 113 | moonbeam: { 114 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=1284&", 115 | token: "GLMR" 116 | }, 117 | moonriver: { 118 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=1285&", 119 | token: "MOVR" 120 | }, 121 | gnosis: { 122 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=100&", 123 | token: "XDAI" 124 | }, 125 | avalanche: { 126 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=43114&", 127 | token: "AVAX" 128 | } 129 | } 130 | 131 | export const L2 = { 132 | optimism: { 133 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=10&", 134 | gasPriceOracle: "0x420000000000000000000000000000000000000F", 135 | token: "ETH" 136 | }, 137 | base: { 138 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=8453&", 139 | gasPriceOracle: "0x420000000000000000000000000000000000000F", 140 | token: "ETH" 141 | }, 142 | arbitrum: { 143 | baseUrl: "https://api.etherscan.io/v2/api?module=proxy&chainid=42161&", 144 | gasPriceOracle: "", 145 | token: "ETH" 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/utils/prices.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import { defaultAbiCoder } from "@ethersproject/abi"; 4 | import { DEFAULT_COINMARKET_BASE_URL, DEFAULT_BLOB_BASE_FEE } from "../constants"; 5 | import { GasReporterOptions } from "../types"; 6 | 7 | import { 8 | warnCMCRemoteCallFailed, 9 | warnGasPriceRemoteCallFailed, 10 | warnBaseFeeRemoteCallFailed, 11 | warnBlobBaseFeeRemoteCallFailed, 12 | warnBaseFeePerByteRemoteCallFailed, 13 | warnUnsupportedChainConfig, 14 | } from "./ui"; 15 | import { hexWeiToIntGwei, getArbitrumBaseFeePerByte } from "./gas"; 16 | import { getTokenForChain, getGasPriceUrlForChain, getBlockUrlForChain, getBlobBaseFeeUrlForChain, getBaseFeePerByteUrlForChain } from "./chains"; 17 | 18 | /** 19 | * Fetches gas, base, & blob fee rates from etherscan as well as current market value of 20 | * network token in nation state currency specified by the options from coinmarketcap 21 | * (defaults to usd). Sets 22 | * 23 | * + options.tokenPrice 24 | * + options.gasPrice 25 | * + options.baseFee 26 | * + options.blobBaseFee 27 | * 28 | * ... unless these are already set as constants in the reporter options 29 | * 30 | * Returns a list of warnings generated if remote calls fail 31 | * @param {GasReporterOptions} options 32 | */ 33 | export async function setGasAndPriceRates(options: GasReporterOptions): Promise { 34 | if ( 35 | (options.offline) || 36 | !options.coinmarketcap || 37 | (!options.L2 && options.tokenPrice && options.gasPrice) || 38 | (options.L2 && options.tokenPrice && options.gasPrice && options.baseFee && options.blobBaseFee) 39 | ) return []; 40 | 41 | let block; 42 | let blockUrl; 43 | let gasPriceUrl; 44 | let blobBaseFeeUrl; 45 | let baseFeePerByteUrl; 46 | const warnings: string[] = []; 47 | 48 | try { 49 | options.token = getTokenForChain(options); 50 | gasPriceUrl = getGasPriceUrlForChain(options); 51 | blockUrl = getBlockUrlForChain(options); 52 | blobBaseFeeUrl = getBlobBaseFeeUrlForChain(options); 53 | baseFeePerByteUrl = getBaseFeePerByteUrlForChain(options); 54 | } catch (err: any){ 55 | if (options.L2) 56 | warnings.push(warnUnsupportedChainConfig(options.L2!)); 57 | else 58 | warnings.push(warnUnsupportedChainConfig(options.L1!)); 59 | 60 | return warnings; 61 | } 62 | 63 | const axiosInstance = axios.create({ 64 | baseURL: DEFAULT_COINMARKET_BASE_URL 65 | }); 66 | 67 | const requestArgs = `latest?symbol=${options.token}&CMC_PRO_API_KEY=${ 68 | options.coinmarketcap 69 | }&convert=`; 70 | 71 | const currencyKey = options.currency!.toUpperCase(); 72 | const currencyPath = `${requestArgs}${currencyKey}`; 73 | 74 | // Currency market data: coinmarketcap 75 | if (!options.tokenPrice) { 76 | try { 77 | const response = await axiosInstance.get(currencyPath); 78 | options.tokenPrice = response.data.data[options.token].quote[ 79 | currencyKey 80 | ].price.toFixed(2); 81 | } catch (error) { 82 | warnings.push(warnCMCRemoteCallFailed(error, DEFAULT_COINMARKET_BASE_URL + currencyPath)); 83 | } 84 | } 85 | 86 | // Gas price data (Etherscan) 87 | if (!options.gasPrice) { 88 | try { 89 | const response = await axiosInstance.get(gasPriceUrl!); 90 | checkForEtherscanError(response.data.result); 91 | const gasPrice = hexWeiToIntGwei(response.data.result); 92 | options.gasPrice = (gasPrice >= 1 ) ? Math.round(gasPrice) : gasPrice;; 93 | } catch (error) { 94 | options.gasPrice = 0; 95 | warnings.push(warnGasPriceRemoteCallFailed(error, gasPriceUrl!)); 96 | } 97 | } 98 | 99 | // baseFeePerByte data: etherscan eth_call to NodeInterface 100 | if ((options.L2 === "arbitrum") && !options.baseFeePerByte) { 101 | try { 102 | const response = await axiosInstance.get(baseFeePerByteUrl); 103 | checkForEtherscanError(response.data.result); 104 | const decoded = defaultAbiCoder.decode(['uint256', 'uint256', 'uint256'], response.data.result); 105 | const baseFeePerByte = getArbitrumBaseFeePerByte(decoded[2]); 106 | options.baseFeePerByte = (baseFeePerByte >= 1 ) ? Math.round(baseFeePerByte) : baseFeePerByte; 107 | } catch (error) { 108 | options.baseFeePerByte = 20; 109 | warnings.push(warnBaseFeePerByteRemoteCallFailed(error)); 110 | } 111 | } 112 | 113 | // baseFee data (Etherscan) 114 | if ((options.L2 === "optimism" || options.L2 === "base") && !options.baseFee) { 115 | try { 116 | block = await axiosInstance.get(blockUrl); 117 | checkForEtherscanError(block.data.result); 118 | const baseFee = hexWeiToIntGwei(block.data.result.baseFeePerGas); 119 | options.baseFee = (baseFee >= 1 ) ? Math.round(baseFee) : baseFee; 120 | } catch (error) { 121 | options.baseFee = 0; 122 | warnings.push(warnBaseFeeRemoteCallFailed(error, blockUrl)); 123 | } 124 | } 125 | 126 | // blobBaseFee data: etherscan eth_call to OP Stack gas oracle on L2 127 | if ( 128 | (options.L2 === "optimism" || options.L2 === "base") && 129 | options.optimismHardfork === "ecotone" && 130 | !options.blobBaseFee 131 | ) { 132 | try { 133 | const response = await axiosInstance.get(blobBaseFeeUrl); 134 | checkForEtherscanError(response.data.result); 135 | const blobBaseFee = hexWeiToIntGwei(response.data.result); 136 | options.blobBaseFee = (blobBaseFee >= 1 ) ? Math.round(blobBaseFee) : blobBaseFee; 137 | } catch (error) { 138 | options.blobBaseFee = DEFAULT_BLOB_BASE_FEE; 139 | warnings.push(warnBlobBaseFeeRemoteCallFailed(error)); 140 | } 141 | } 142 | 143 | return warnings; 144 | } 145 | 146 | function checkForEtherscanError(res: string) { 147 | if (typeof res === "string" && !res.includes("0x")){ 148 | throw new Error(res); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/utils/sources.ts: -------------------------------------------------------------------------------- 1 | import { SolcConfig } from "hardhat/types"; 2 | import { keccak256 } from "ethereum-cryptography/keccak"; 3 | import { utf8ToBytes, bytesToHex } from "ethereum-cryptography/utils"; 4 | import { SolcInfo } from "../types"; 5 | 6 | 7 | /** 8 | * Generates hashed function selector from the human readable function signature 9 | * @param {string} fnSig 10 | * @returns 11 | */ 12 | export function getHashedFunctionSignature(fnSig: string ): string { 13 | return bytesToHex(keccak256(Buffer.from(utf8ToBytes(fnSig)))).slice(0, 8); 14 | } 15 | 16 | /** 17 | * Generates id for a GasData.methods entry from the input of a web3.eth.getTransaction 18 | * and a contract name 19 | * @param {String} contractName 20 | * @param {String} code hex data 21 | * @return {String} id 22 | */ 23 | export function getMethodID(contractName: string, code: string): string { 24 | return `${contractName }_${ code.slice(2, 10)}`; 25 | } 26 | 27 | /** 28 | * Extracts solc settings and version info from solidity metadata 29 | * @param {Object} solcConfig solidity config 30 | * @return {Object} {version, optimizer, runs} 31 | */ 32 | export function getSolcInfo(solcConfig: SolcConfig): SolcInfo { 33 | const info: any = {}; 34 | const optimizer = solcConfig.settings.optimizer; 35 | const viaIR = solcConfig.settings.viaIR 36 | 37 | if (solcConfig) { 38 | info.version = solcConfig.version; 39 | info.optimizer = (optimizer) ? optimizer.enabled : "----" 40 | info.runs = (optimizer) ? optimizer.runs : "----" 41 | info.viaIR = (viaIR !== undefined) ? viaIR : false; 42 | } 43 | return info; 44 | } 45 | 46 | /** 47 | * Return true if transaction input and bytecode are same, ignoring library link code. 48 | * @param {String} input contract creation tx `input` 49 | * @param {String} bytecode contract bytecode 50 | * @return {Bool} 51 | */ 52 | export function matchBinaries(input: string, bytecode: string): boolean { 53 | const regExp = bytecodeToBytecodeRegex(bytecode); 54 | return input.match(regExp) !== null; 55 | } 56 | 57 | /** 58 | * Generate a regular expression string which is library link agnostic so we can match 59 | * linked bytecode deployment transaction inputs to the evm.bytecode solc output. 60 | * @param {String} bytecode 61 | * @return {String} 62 | */ 63 | export function bytecodeToBytecodeRegex(bytecode = ""): string { 64 | const bytecodeRegex = bytecode 65 | .replace(/__.{38}/g, ".{40}") 66 | .replace(/73f{40}/g, ".{42}"); 67 | 68 | // HACK: Node regexes can't be longer that 32767 characters. 69 | // Contracts bytecode can. We just truncate the regexes. It's safe in practice. 70 | const MAX_REGEX_LENGTH = 32767; 71 | const truncatedBytecodeRegex = bytecodeRegex.slice(0, MAX_REGEX_LENGTH); 72 | return truncatedBytecodeRegex; 73 | } 74 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { resetHardhatContext } from "hardhat/plugins-testing"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { Deployment, MethodData, MethodDataItem } from "./types"; 4 | 5 | declare module "mocha" { 6 | interface Context { 7 | env: HardhatRuntimeEnvironment; 8 | } 9 | } 10 | 11 | export function findMethod( 12 | methods: MethodData, 13 | contractName: string, 14 | methodName: string 15 | ) : MethodDataItem | null { 16 | for (const key of Object.keys(methods)){ 17 | if (methods[key].contract === contractName && methods[key].method === methodName) { 18 | return methods[key]; 19 | } 20 | } 21 | return null; 22 | } 23 | 24 | export function findDeployment( 25 | deployments: Deployment[], 26 | contractName: string, 27 | ) : Deployment | null { 28 | for (const deployment of deployments){ 29 | if (deployment.name === contractName) { 30 | return deployment; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | export function useEnvironment( 37 | projectPath: string, 38 | networkName?: string, 39 | configPath?: string 40 | ) { 41 | before("Loading hardhat environment", async function () { 42 | process.chdir(projectPath); 43 | 44 | process.env.CI = "true"; 45 | 46 | if (networkName !== undefined) { 47 | process.env.HARDHAT_NETWORK = networkName; 48 | } 49 | 50 | if (configPath !== undefined) { 51 | process.env.HARDHAT_CONFIG = configPath 52 | } 53 | 54 | this.env = require("hardhat"); 55 | }); 56 | 57 | after("Resetting hardhat", function () { 58 | resetHardhatContext(); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/integration/forked.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { execSync } from "child_process"; 4 | import { readFileSync } from "fs"; 5 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 6 | import path from "path"; 7 | 8 | import { GasReporterOutput, MethodData } from "../types"; 9 | import { useEnvironment, findMethod } from "../helpers"; 10 | 11 | describe("Forked Networks: remoteContract, hardhat_reset", function () { 12 | let output: GasReporterOutput; 13 | let methods: MethodData; 14 | 15 | const projectPath = path.resolve( 16 | __dirname, 17 | "../projects/forked" 18 | ); 19 | 20 | const outputPath = path.resolve( 21 | __dirname, 22 | "../projects/forked/gasReporterOutput.json" 23 | ); 24 | 25 | const network = undefined; 26 | const configPath = "./hardhat.config.ts"; 27 | 28 | useEnvironment(projectPath, network, configPath); 29 | 30 | before(async function(){ 31 | await this.env.run(TASK_TEST, { testFiles: [] }); 32 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 33 | methods = output.data!.methods; 34 | }) 35 | 36 | after(() => execSync(`rm ${outputPath}`)); 37 | 38 | it("calls remoteContract WETH.deposit", function(){ 39 | const method = findMethod(methods, "WETH", "deposit"); 40 | assert(method?.numberOfCalls! > 0); 41 | }) 42 | 43 | it("preserves data about calls between hardhat_reset invocations", function(){ 44 | const method = findMethod(methods, "ContractA", "sendFn"); 45 | assert(method?.numberOfCalls! === 2); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/integration/node.ts: -------------------------------------------------------------------------------- 1 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { GasReporterOptions, GasReporterOutput } from "../types"; 5 | 6 | import { useEnvironment } from "./../helpers"; 7 | 8 | describe("Independent Node: Hardhat", function () { 9 | let output: GasReporterOutput; 10 | let options: GasReporterOptions; 11 | 12 | const projectPath = path.resolve( 13 | __dirname, 14 | "../projects/options" 15 | ); 16 | const outputPath = path.resolve( 17 | __dirname, 18 | "../projects/options/gas.json" 19 | ); 20 | 21 | const network = undefined; 22 | const configPath = "./hardhat.options.b.config.ts"; 23 | 24 | useEnvironment(projectPath, network, configPath); 25 | 26 | before(async function(){ 27 | if(!process.env.STAND_ALONE) this.skip(); 28 | 29 | await this.env.run(TASK_TEST, { testFiles: [] }); 30 | output = JSON.parse(fs.readFileSync(outputPath, 'utf-8')); 31 | options = output.options; 32 | }) 33 | 34 | it("wrote to file", function () { 35 | if(!process.env.STAND_ALONE) this.skip(); 36 | 37 | const outputFileOption = options.outputFile; 38 | const outputFilePath = path.resolve( 39 | __dirname, 40 | `../projects/options/${outputFileOption!}` 41 | ); 42 | 43 | const file = fs.readFileSync(outputFilePath, "utf8"); 44 | console.log(file); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/integration/options.a.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { readFileSync } from "fs"; 4 | import { execSync } from "child_process"; 5 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 6 | import path from "path"; 7 | import { Deployment, GasReporterOptions, GasReporterOutput, MethodData } from "../types"; 8 | 9 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 10 | 11 | /** 12 | * OPTIONS ARE SET UP TO TEST: 13 | * + Complex compiler details 14 | * + Non-ethereum L1 (polygon) with live market price 15 | * + Custom gasPrice API call 16 | * + Exclude contracts from reporting 17 | * + Display full method signature 18 | * + Dark mode 19 | * + RST titles 20 | * + Gas deltas 21 | */ 22 | describe("Options A", function () { 23 | let output: GasReporterOutput; 24 | let options: GasReporterOptions; 25 | let methods: MethodData; 26 | let deployments: Deployment[]; 27 | 28 | const projectPath = path.resolve( 29 | __dirname, 30 | "../projects/options" 31 | ); 32 | 33 | const outputPath = path.resolve( 34 | __dirname, 35 | "../projects/options/gasReporterOutput.json" 36 | ); 37 | 38 | const network = undefined; 39 | const configPath = "./hardhat.options.a.config.ts"; 40 | 41 | useEnvironment(projectPath, network, configPath); 42 | 43 | before(async function(){ 44 | await this.env.run(TASK_TEST, { testFiles: [] }); 45 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 46 | options = output.options; 47 | methods = output.data!.methods; 48 | deployments = output.data!.deployments; 49 | }); 50 | 51 | after(() => execSync(`rm ${outputPath}`)); 52 | 53 | it("fetched a currency price", function () { 54 | assert.exists(options.tokenPrice); 55 | assert.isNumber(parseFloat(options.tokenPrice!)); 56 | }); 57 | 58 | it("fetched a gas price", function() { 59 | assert.exists(options.gasPrice); 60 | assert.isNumber(options.gasPrice); 61 | }); 62 | 63 | it("calculates costs for method calls", function(){ 64 | const method = findMethod(methods, "VariableCosts", "addToMap"); 65 | assert.isNumber(parseFloat(method!.cost!)); 66 | }); 67 | 68 | it("calculates costs for deployments", function(){ 69 | const deployment = findDeployment(deployments, "VariableConstructor"); 70 | assert.isNumber(parseFloat(deployment!.cost!)); 71 | }); 72 | 73 | it("includes bytecode and deployedBytecode in deployment data", function(){ 74 | const deployment = findDeployment(deployments, "VariableConstructor"); 75 | assert.isString(deployment?.bytecode); 76 | assert.isString(deployment?.deployedBytecode); 77 | }); 78 | 79 | it("excludes `excludedContracts` from report", function(){ 80 | const deployment = findDeployment(deployments, "EtherRouter"); 81 | assert.isNull(deployment); 82 | }); 83 | 84 | it("resolves shadowed method calls with the example proxy resolver, and with factory deployed contracts", function(){ 85 | const methodA = findMethod(methods, "VersionA", "setValue"); 86 | const methodB = findMethod(methods, "VersionB", "setValue"); 87 | 88 | assert.equal(methodA?.numberOfCalls, 2); 89 | assert.equal(methodB?.numberOfCalls, 2); 90 | }); 91 | 92 | it("calculates gas deltas for methods and deployments", async function(){ 93 | process.env.GAS_DELTA = "true"; 94 | await this.env.run(TASK_TEST, { testFiles: [] }); 95 | process.env.GAS_DELTA = ""; 96 | 97 | const _output = JSON.parse(readFileSync(options.cachePath!, 'utf-8')); 98 | const _methods = _output.data!.methods; 99 | const _deployments = _output.data!.deployments; 100 | const _options = _output.options; 101 | 102 | const method = findMethod(_methods, "VariableCosts", "addToMap"); 103 | const deployment = findDeployment(_deployments, "VariableConstructor"); 104 | 105 | 106 | if (_options.cachePath) { 107 | try { 108 | execSync(`rm ${_options.cachePath}`) 109 | } catch {} 110 | } 111 | 112 | assert.isNumber(method!.executionGasAverageDelta!); 113 | assert.notEqual(method!.executionGasAverageDelta!, 0); 114 | 115 | assert.isNumber(deployment!.executionGasAverage); 116 | assert.notEqual(deployment!.executionGasAverageDelta, 0); 117 | }); 118 | 119 | }); 120 | -------------------------------------------------------------------------------- /test/integration/options.b.ts: -------------------------------------------------------------------------------- 1 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import { assert } from "chai"; 4 | import { execSync } from "child_process"; 5 | import path from "path"; 6 | import fs from "fs"; 7 | import { GasReporterOptions, GasReporterOutput } from "../types"; 8 | 9 | import { useEnvironment } from "../helpers"; 10 | 11 | /** 12 | * OPTIONS ARE SET UP TO TEST: 13 | * + user-configured token and gasPrice 14 | * + write-to-custom-file-name (JSON & txt) 15 | * + force terminal output w/ custom output 16 | * + show uncalled methods 17 | */ 18 | describe("Options B", function () { 19 | let output: GasReporterOutput; 20 | let options: GasReporterOptions; 21 | 22 | const projectPath = path.resolve( 23 | __dirname, 24 | "../projects/options" 25 | ); 26 | 27 | // NB: test sets the outputJSONFile option 28 | const outputPath = path.resolve( 29 | __dirname, 30 | "../projects/options/gas.json" 31 | ); 32 | 33 | const network = undefined; 34 | const configPath = "./hardhat.options.b.config.ts"; 35 | 36 | useEnvironment(projectPath, network, configPath); 37 | 38 | before(async function(){ 39 | await this.env.run(TASK_TEST, { testFiles: [] }); 40 | output = JSON.parse(fs.readFileSync(outputPath, 'utf-8')); 41 | options = output.options; 42 | }) 43 | 44 | after(() => execSync(`rm ${outputPath}`)); 45 | 46 | it("set the options correctly", function(){ 47 | assert.equal(options.token, "ETC"); 48 | assert.equal(options.tokenPrice, "200.00"); 49 | assert.equal(options.gasPrice, 40); 50 | assert.equal(options.forceTerminalOutput, true); 51 | assert.equal(options.forceTerminalOutputFormat, 'legacy'); 52 | assert.equal(options.reportFormat, 'terminal'); 53 | }); 54 | 55 | it("wrote table to file", function () { 56 | const outputFileOption = options.outputFile; 57 | const outputFilePath = path.resolve( 58 | __dirname, 59 | `../projects/options/${outputFileOption!}` 60 | ); 61 | 62 | const file = fs.readFileSync(outputFilePath, "utf8"); 63 | assert.isString(file); 64 | assert.isAbove(file.length, 100); 65 | 66 | // Should be decolorized while terminal output in full color 67 | console.log(file); 68 | 69 | // Clean up 70 | execSync(`rm ${outputFilePath}`); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/integration/options.c.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | // TODO: REMOVE LINT DISABLE 3 | /* eslint-disable */ 4 | import { assert } from "chai"; 5 | import { execSync } from "child_process"; 6 | import { readFileSync } from "fs"; 7 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 8 | import path from "path"; 9 | import { Deployment, GasReporterOptions, GasReporterOutput, MethodData } from "../types"; 10 | 11 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 12 | 13 | /** 14 | * OPTIONS ARE SET UP TO TEST: 15 | * + Markdown format 16 | * + L2: Optimism 17 | * + Live market prices in CHF 18 | * + User configured gasPrice and baseGas 19 | */ 20 | describe("Options C", function () { 21 | let output: GasReporterOutput; 22 | let options: GasReporterOptions; 23 | let methods: MethodData; 24 | let deployments: Deployment[]; 25 | 26 | const projectPath = path.resolve( 27 | __dirname, 28 | "../projects/options" 29 | ); 30 | 31 | const outputPath = path.resolve( 32 | __dirname, 33 | "../projects/options/gasReporterOutput.json" 34 | ); 35 | 36 | const network = undefined; 37 | const configPath = "./hardhat.options.c.config.ts"; 38 | 39 | useEnvironment(projectPath, network, configPath); 40 | 41 | before(async function(){ 42 | await this.env.run(TASK_TEST, { testFiles: [] }); 43 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 44 | options = output.options; 45 | methods = output.data!.methods; 46 | deployments = output.data!.deployments; 47 | }) 48 | 49 | after(() => execSync(`rm ${outputPath}`)); 50 | 51 | it("prints", function () { 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/integration/options.default.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { execSync } from "child_process"; 4 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 5 | import path from "path"; 6 | 7 | import { readFileSync } from "fs"; 8 | import { 9 | DEFAULT_CURRENCY_DISPLAY_PRECISION, 10 | DEFAULT_JSON_OUTPUT_FILE, 11 | TABLE_NAME_TERMINAL 12 | } from "../../src/constants"; 13 | 14 | import { Deployment, GasReporterOptions, GasReporterOutput, MethodData } from "../../src/types"; 15 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 16 | 17 | describe("Default Options", function () { 18 | let output: GasReporterOutput; 19 | let options: GasReporterOptions; 20 | let methods: MethodData; 21 | let deployments: Deployment[]; 22 | 23 | const projectPath = path.resolve( 24 | __dirname, 25 | "../projects/options" 26 | ); 27 | 28 | const outputPath = path.resolve( 29 | __dirname, 30 | "../projects/options/gasReporterOutput.json" 31 | ); 32 | 33 | const network = undefined; 34 | const configPath = "./hardhat.default.config.ts"; 35 | 36 | useEnvironment(projectPath, network, configPath); 37 | 38 | before(async function(){ 39 | await this.env.run(TASK_TEST, { testFiles: [] }); 40 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 41 | options = output.options; 42 | methods = output.data!.methods; 43 | deployments = output.data!.deployments; 44 | }) 45 | 46 | after(() => execSync(`rm ${outputPath}`)); 47 | 48 | it("default options", async function () { 49 | assert.equal(options.currency, "USD"); 50 | assert.equal(options.enabled, true); 51 | assert.deepEqual(options.excludeContracts, []); 52 | assert.equal(options.noColors, false); 53 | assert.equal(options.showUncalledMethods, false); 54 | assert.equal(options.rst, false); 55 | assert.equal(options.rstTitle, ""); 56 | assert.equal(options.showMethodSig, false); 57 | assert.equal(options.outputJSON, false); 58 | assert.equal(options.outputJSONFile, DEFAULT_JSON_OUTPUT_FILE); 59 | assert.equal(options.darkMode, false); 60 | assert.equal(options.reportFormat, TABLE_NAME_TERMINAL); 61 | assert.equal(options.currencyDisplayPrecision, DEFAULT_CURRENCY_DISPLAY_PRECISION ); 62 | assert.equal(options.offline, false); 63 | assert.equal(options.forceTerminalOutput, false); 64 | assert.equal(options.includeIntrinsicGas, true); 65 | assert.equal(options.reportPureAndViewMethods, false); 66 | assert.equal(options.excludeAutoGeneratedGetters, false); 67 | assert.equal(options.L1, "ethereum"); 68 | assert.equal(options.includeBytecodeInJSON, false); 69 | 70 | // Make sure we didn't hit endpoint 71 | assert.equal(options.gasPrice, undefined); 72 | }); 73 | 74 | it("includes the package version in json output", () => { 75 | const pkg = require("../../package.json"); 76 | assert.equal(output.version, pkg.version); 77 | }); 78 | 79 | it("should collect method data for contract names that shadow each other", function(){ 80 | const dataItemA = findMethod(methods, "DuplicateA.sol:Duplicate", "a"); 81 | const dataItemB = findMethod(methods, "DuplicateB.sol:Duplicate", "b"); 82 | 83 | // Also checking that there's no doubling here 84 | assert(dataItemA!.numberOfCalls === 1); 85 | assert(dataItemB!.numberOfCalls === 1); 86 | }); 87 | 88 | it("should collect method data for contracts that use immutable vars", function(){ 89 | const dataItemA = findMethod(methods, "Immutable", "setVal"); 90 | assert(dataItemA!.numberOfCalls === 1); 91 | }); 92 | 93 | // methodThatThrows is called twice: success and failure - we should only see one call though. 94 | it ("should *not* record transactions that revert", function(){ 95 | const dataItem = findMethod(methods, "VariableCosts", "methodThatThrows"); 96 | 97 | assert.equal(dataItem?.numberOfCalls, 1) 98 | }); 99 | 100 | // getBalance is called in the tests 101 | it ("should *not* record view/pure methods (by default)", function(){ 102 | const dataItem = findMethod(methods, "VariableCosts", "getBalance"); 103 | 104 | assert.equal(dataItem?.numberOfCalls, 0); 105 | }); 106 | 107 | it("should collect method data for multiple calls and set min, max, avg", function(){ 108 | const dataItem = findMethod(methods, "VariableCosts", "addToMap"); 109 | assert.equal(dataItem?.numberOfCalls, 4); 110 | assert.equal(dataItem?.gasData.length, 4); 111 | assert.equal(dataItem?.intrinsicGas.length, 4); 112 | assert.exists(dataItem?.min); 113 | assert.exists(dataItem?.max); 114 | assert.exists(dataItem?.executionGasAverage); 115 | assert(dataItem!.min! < dataItem!.max!); 116 | assert(dataItem!.min! < dataItem!.executionGasAverage!); 117 | assert(dataItem!.executionGasAverage! < dataItem!.max!); 118 | assert(dataItem?.intrinsicGas[0]! > 21_000); 119 | assert(dataItem?.gasData[0]! > dataItem?.intrinsicGas[0]!); 120 | }); 121 | 122 | it("should collect deployment data for contracts with names that shadow each other", function(){ 123 | const deploymentA = findDeployment(deployments, "DuplicateA.sol:Duplicate"); 124 | const deploymentB = findDeployment(deployments, "DuplicateB.sol:Duplicate"); 125 | 126 | assert(deploymentA!.gasData.length > 0); 127 | assert(deploymentB!.gasData.length > 0); 128 | }); 129 | 130 | it("should collect deployment data for contracts that use immutable vars", function(){ 131 | const deployment = findDeployment(deployments, "Immutable"); 132 | assert(deployment!.gasData.length > 0); 133 | }); 134 | 135 | it("should collect deployment data for multiple deployments and set min, max, avg", function(){ 136 | const deployment = findDeployment(deployments, "VariableConstructor"); 137 | 138 | assert(deployment?.gasData!.length! > 1); 139 | assert.exists(deployment?.min); 140 | assert.exists(deployment?.max); 141 | assert.exists(deployment?.executionGasAverage); 142 | assert(deployment!.min! < deployment!.max!); 143 | assert(deployment!.min! < deployment!.executionGasAverage!); 144 | assert(deployment!.executionGasAverage! < deployment!.max!) 145 | assert(deployment!.percent! > 0); 146 | }); 147 | 148 | it ("should delete the bytecode & deployedBytecode", function() { 149 | const deployment = findDeployment(deployments, "VariableConstructor"); 150 | 151 | assert.isUndefined(deployment?.bytecode); 152 | assert.isUndefined(deployment?.deployedBytecode); 153 | }) 154 | }); 155 | -------------------------------------------------------------------------------- /test/integration/options.e.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | // TODO: REMOVE LINT DISABLE 3 | /* eslint-disable */ 4 | import { assert } from "chai"; 5 | import { execSync } from "child_process"; 6 | import { readFileSync } from "fs"; 7 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 8 | import path from "path"; 9 | import { Deployment, GasReporterOptions, GasReporterOutput, MethodData } from "../types"; 10 | 11 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 12 | 13 | /** 14 | * OPTIONS ARE SET UP TO TEST: 15 | * + Default Terminal Format 16 | * + L2: Optimism 17 | * + reportPureAndViewMethods 18 | * + excludeAutoGeneratedGetters 19 | */ 20 | describe("Options E (OPStack:optimism with live pricing & `reportPureAndViewMethods`)", function () { 21 | let output: GasReporterOutput; 22 | let options: GasReporterOptions; 23 | let methods: MethodData; 24 | let deployments: Deployment[]; 25 | 26 | const projectPath = path.resolve( 27 | __dirname, 28 | "../projects/options" 29 | ); 30 | 31 | const outputPath = path.resolve( 32 | __dirname, 33 | "../projects/options/gasReporterOutput.json" 34 | ); 35 | 36 | const network = undefined; 37 | const configPath = "./hardhat.options.e.config.ts"; 38 | 39 | useEnvironment(projectPath, network, configPath); 40 | 41 | before(async function(){ 42 | await this.env.run(TASK_TEST, { testFiles: [] }); 43 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 44 | options = output.options; 45 | methods = output.data!.methods; 46 | deployments = output.data!.deployments; 47 | }) 48 | 49 | after(() => execSync(`rm ${outputPath}`)); 50 | 51 | it("auto-configures options correctly", function () { 52 | assert.equal(options.L2, "optimism"); 53 | assert.equal(options.optimismHardfork, "ecotone"); 54 | assert.isDefined(options.gasPrice); 55 | assert.isDefined(options.blobBaseFee); 56 | assert.isBelow(options.gasPrice!, 1); 57 | assert.isAbove(options.blobBaseFee!, 0); 58 | 59 | assert.isDefined(options.tokenPrice); 60 | assert.isAbove(parseFloat(options.tokenPrice!), 1000); // Eth-ish 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/integration/options.f.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | // TODO: REMOVE LINT DISABLE 3 | /* eslint-disable */ 4 | import { assert } from "chai"; 5 | import { execSync } from "child_process"; 6 | import { readFileSync } from "fs"; 7 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 8 | import path from "path"; 9 | import { Deployment, GasReporterOptions, GasReporterOutput, MethodData } from "../types"; 10 | 11 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 12 | 13 | /** 14 | * OPTIONS ARE SET UP TO TEST: 15 | * + Default Terminal Format 16 | * + L2: Base 17 | * + reportPureAndViewMethods 18 | * + excludeAutoGeneratedGetters 19 | */ 20 | describe("Options F (OPStack:base with live pricing & `reportPureAndViewMethods`)", function () { 21 | let output: GasReporterOutput; 22 | let options: GasReporterOptions; 23 | let methods: MethodData; 24 | let deployments: Deployment[]; 25 | 26 | const projectPath = path.resolve( 27 | __dirname, 28 | "../projects/options" 29 | ); 30 | 31 | const outputPath = path.resolve( 32 | __dirname, 33 | "../projects/options/gasReporterOutput.json" 34 | ); 35 | 36 | const network = undefined; 37 | const configPath = "./hardhat.options.f.config.ts"; 38 | 39 | useEnvironment(projectPath, network, configPath); 40 | 41 | before(async function(){ 42 | await this.env.run(TASK_TEST, { testFiles: [] }); 43 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 44 | options = output.options; 45 | methods = output.data!.methods; 46 | deployments = output.data!.deployments; 47 | }) 48 | 49 | after(() => execSync(`rm ${outputPath}`)); 50 | 51 | it("auto-configures options correctly", function () { 52 | assert.equal(options.L2, "base"); 53 | assert.equal(options.optimismHardfork, "ecotone"); 54 | assert.isDefined(options.gasPrice); 55 | assert.isDefined(options.blobBaseFee); 56 | assert.isBelow(options.gasPrice!, 1); 57 | assert.isAbove(options.blobBaseFee!, 0); 58 | 59 | assert.isDefined(options.tokenPrice); 60 | assert.isAbove(parseFloat(options.tokenPrice!), 1000); // Eth-ish 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/integration/options.g.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | // TODO: REMOVE LINT DISABLE 3 | /* eslint-disable */ 4 | import { assert } from "chai"; 5 | import { execSync } from "child_process"; 6 | import { readFileSync } from "fs"; 7 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 8 | import path from "path"; 9 | import { Deployment, GasReporterOptions, GasReporterOutput, MethodData } from "../types"; 10 | 11 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 12 | 13 | /** 14 | * OPTIONS ARE SET UP TO TEST: 15 | * + Default Terminal Format 16 | * + L2: Arbitrum 17 | * + reportPureAndViewMethods 18 | * + excludeAutoGeneratedGetters 19 | */ 20 | describe("Options G (Arbitrum with live pricing & `reportPureAndViewMethods`)", function () { 21 | let output: GasReporterOutput; 22 | let options: GasReporterOptions; 23 | let methods: MethodData; 24 | let deployments: Deployment[]; 25 | 26 | const projectPath = path.resolve( 27 | __dirname, 28 | "../projects/options" 29 | ); 30 | 31 | const outputPath = path.resolve( 32 | __dirname, 33 | "../projects/options/gasReporterOutput.json" 34 | ); 35 | 36 | const network = undefined; 37 | const configPath = "./hardhat.options.g.config.ts"; 38 | 39 | useEnvironment(projectPath, network, configPath); 40 | 41 | before(async function(){ 42 | await this.env.run(TASK_TEST, { testFiles: [] }); 43 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 44 | options = output.options; 45 | methods = output.data!.methods; 46 | deployments = output.data!.deployments; 47 | }) 48 | 49 | after(() => execSync(`rm ${outputPath}`)); 50 | 51 | it("auto-configures options correctly", function () { 52 | assert.equal(options.L2, "arbitrum"); 53 | assert.isDefined(options.gasPrice); 54 | assert.isDefined(options.baseFeePerByte); 55 | assert.isBelow(options.gasPrice!, 1); 56 | assert(options.baseFeePerByte! >= 0); // This is sometimes 0 ? 57 | 58 | assert.isDefined(options.tokenPrice); 59 | assert.isAbove(parseFloat(options.tokenPrice!), 1000); // Eth-ish 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/integration/oz.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { execSync } from "child_process"; 4 | import { readFileSync } from "fs"; 5 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 6 | import path from "path"; 7 | 8 | import { Deployment, GasReporterOutput, MethodData } from "../types"; 9 | 10 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 11 | 12 | describe("OZ Upgrades", function () { 13 | let output: GasReporterOutput; 14 | let methods: MethodData; 15 | let deployments: Deployment[]; 16 | 17 | const projectPath = path.resolve( 18 | __dirname, 19 | "../projects/oz" 20 | ); 21 | 22 | const outputPath = path.resolve( 23 | __dirname, 24 | "../projects/oz/gasReporterOutput.json" 25 | ); 26 | 27 | const network = undefined; 28 | const configPath = "./hardhat.config.ts"; 29 | 30 | useEnvironment(projectPath, network, configPath); 31 | 32 | before(async function(){ 33 | await this.env.run(TASK_TEST, { testFiles: [] }); 34 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 35 | methods = output.data!.methods; 36 | deployments = output.data!.deployments; 37 | }) 38 | 39 | after(() => execSync(`rm ${outputPath}`)); 40 | 41 | it ("should record shadowed transactions made with proxied upgrade system", function(){ 42 | const firstBoxMethod = findMethod(methods, "ProxyBox", "setBox"); 43 | const secondBoxMethod = findMethod(methods, "ProxyBoxV2", "setBox"); 44 | 45 | assert.equal(firstBoxMethod?.numberOfCalls, 1); 46 | assert.equal(secondBoxMethod?.numberOfCalls, 1); 47 | }); 48 | 49 | it ("should record deployments made with proxied upgrade system", function(){ 50 | const firstBoxDeployment = findDeployment(deployments, "ProxyBox"); 51 | const secondBoxDeployment = findDeployment(deployments, "ProxyBoxV2"); 52 | 53 | assert.isNotNull(firstBoxDeployment); 54 | assert.isNotNull(secondBoxDeployment); 55 | 56 | assert(firstBoxDeployment!.gasData.length > 0); 57 | assert(secondBoxDeployment!.gasData.length > 0) 58 | }); 59 | 60 | it ("should record shadowed transactions made with beacon system", function(){ 61 | const firstBoxMethod = findMethod(methods, "BeaconBox", "setBox"); 62 | const secondBoxMethod = findMethod(methods, "BeaconBoxV2", "setBox"); 63 | const thirdBoxMethod = findMethod(methods, "BeaconBoxV2", "setBeaconId"); 64 | 65 | assert.equal(firstBoxMethod?.numberOfCalls, 1); 66 | assert.equal(secondBoxMethod?.numberOfCalls, 1); 67 | assert.equal(thirdBoxMethod?.numberOfCalls, 1); 68 | }); 69 | 70 | it ("should record deployments made with beacon system", function(){ 71 | const firstBoxDeployment = findDeployment(deployments, "BeaconBox"); 72 | const secondBoxDeployment = findDeployment(deployments, "BeaconBoxV2"); 73 | 74 | assert.isNotNull(firstBoxDeployment); 75 | assert.isNotNull(secondBoxDeployment); 76 | 77 | assert(firstBoxDeployment!.gasData.length > 0); 78 | assert(secondBoxDeployment!.gasData.length > 0) 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/integration/parallel.ts: -------------------------------------------------------------------------------- 1 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 2 | import path from "path"; 3 | 4 | import { useEnvironment } from "../helpers"; 5 | 6 | describe("--parallel", function () { 7 | const projectPath = path.resolve( 8 | __dirname, 9 | "../projects/viem" 10 | ); 11 | 12 | const network = undefined; 13 | const configPath = "./hardhat.config.ts"; 14 | 15 | useEnvironment(projectPath, network, configPath); 16 | 17 | before(async function(){ 18 | await this.env.run(TASK_TEST, { parallel: true, testFiles: [] }); 19 | }) 20 | 21 | it ("should complete successfully", function(){}); 22 | }); 23 | -------------------------------------------------------------------------------- /test/integration/run.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | // TODO: REMOVE LINT DISABLE 3 | /* eslint-disable */ 4 | import { assert } from "chai"; 5 | import { execSync } from "child_process"; 6 | import { readFileSync } from "fs"; 7 | import { TASK_RUN } from "hardhat/builtin-tasks/task-names"; 8 | import path from "path"; 9 | 10 | import { Deployment, GasReporterOutput, MethodData } from "../types"; 11 | 12 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 13 | 14 | describe.skip("Flashswap (TASK_HARDHAT_RUN)", function () { 15 | let output: GasReporterOutput; 16 | let methods: MethodData; 17 | let deployments: Deployment[]; 18 | 19 | const projectPath = path.resolve( 20 | __dirname, 21 | "../projects/run" 22 | ); 23 | 24 | const outputPath = path.resolve( 25 | __dirname, 26 | "../projects/run/gasReporterOutput.json" 27 | ); 28 | 29 | const network = undefined; 30 | const configPath = "./hardhat.config.ts"; 31 | 32 | useEnvironment(projectPath, network, configPath); 33 | 34 | before(async function(){ 35 | await this.env.run(TASK_RUN, { script: "script.ts" }); 36 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 37 | methods = output.data!.methods; 38 | deployments = output.data!.deployments; 39 | }) 40 | 41 | after(() => execSync(`rm ${outputPath}`)); 42 | 43 | it ("should print", () => { 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/integration/viem.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { execSync } from "child_process"; 4 | import { readFileSync } from "fs"; 5 | import { TASK_TEST } from "hardhat/builtin-tasks/task-names"; 6 | import path from "path"; 7 | 8 | import { Deployment, GasReporterOutput, MethodData } from "../types"; 9 | 10 | import { useEnvironment, findMethod, findDeployment } from "../helpers"; 11 | 12 | describe("Viem", function () { 13 | let output: GasReporterOutput; 14 | let methods: MethodData; 15 | let deployments: Deployment[]; 16 | 17 | const projectPath = path.resolve( 18 | __dirname, 19 | "../projects/viem" 20 | ); 21 | 22 | const outputPath = path.resolve( 23 | __dirname, 24 | "../projects/viem/gasReporterOutput.json" 25 | ); 26 | 27 | const network = undefined; 28 | const configPath = "./hardhat.config.ts"; 29 | 30 | useEnvironment(projectPath, network, configPath); 31 | 32 | before(async function(){ 33 | await this.env.run(TASK_TEST, { testFiles: [] }); 34 | output = JSON.parse(readFileSync(outputPath, 'utf-8')); 35 | methods = output.data!.methods; 36 | deployments = output.data!.deployments; 37 | }) 38 | 39 | after(() => execSync(`rm ${outputPath}`)); 40 | 41 | it ("should record transactions made with the publicClient", function(){ 42 | const method = findMethod(methods, "Greeter", "setGreeting"); 43 | assert.equal(method?.numberOfCalls, 1); 44 | }); 45 | 46 | it ("should record transactions made with a walletClient", function(){ 47 | const method = findMethod(methods, "Greeter", "asOther"); 48 | assert.equal(method?.numberOfCalls, 1); 49 | }); 50 | 51 | it ("should record deployments", function(){ 52 | const deployment = findDeployment(deployments, "Greeter"); 53 | assert.isNotNull(deployment); 54 | assert(deployment!.gasData.length > 0); 55 | }); 56 | 57 | it ("should record methods executed with eth_call", function(){ 58 | const greet = findMethod(methods, "Greeter", "greet"); 59 | 60 | assert.equal(greet?.numberOfCalls, 2); 61 | assert.equal(greet?.executionGasAverage, 3475); 62 | }); 63 | 64 | it ("should filter auto generated getters", () => { 65 | const greeting = findMethod(methods, "Greeter", "greeting"); 66 | const urGreeting1 = findMethod(methods, "Greeter", "urGreeting"); 67 | const urGreeting2 = findMethod(methods, "EmphaticGreeter", "urGreeting"); 68 | 69 | assert.isNull(greeting); 70 | assert.isNull(urGreeting1); 71 | assert.isNull(urGreeting2); 72 | }) 73 | }); 74 | -------------------------------------------------------------------------------- /test/projects/forked/abi.ts: -------------------------------------------------------------------------------- 1 | export const ABI: any = { 2 | wethABI: [ 3 | { 4 | constant: true, 5 | inputs: [], 6 | name: "name", 7 | outputs: [ 8 | { 9 | name: "", 10 | type: "string", 11 | }, 12 | ], 13 | payable: false, 14 | stateMutability: "view", 15 | type: "function", 16 | }, 17 | { 18 | constant: false, 19 | inputs: [ 20 | { 21 | name: "guy", 22 | type: "address", 23 | }, 24 | { 25 | name: "wad", 26 | type: "uint256", 27 | }, 28 | ], 29 | name: "approve", 30 | outputs: [ 31 | { 32 | name: "", 33 | type: "bool", 34 | }, 35 | ], 36 | payable: false, 37 | stateMutability: "nonpayable", 38 | type: "function", 39 | }, 40 | { 41 | constant: true, 42 | inputs: [], 43 | name: "totalSupply", 44 | outputs: [ 45 | { 46 | name: "", 47 | type: "uint256", 48 | }, 49 | ], 50 | payable: false, 51 | stateMutability: "view", 52 | type: "function", 53 | }, 54 | { 55 | constant: false, 56 | inputs: [ 57 | { 58 | name: "src", 59 | type: "address", 60 | }, 61 | { 62 | name: "dst", 63 | type: "address", 64 | }, 65 | { 66 | name: "wad", 67 | type: "uint256", 68 | }, 69 | ], 70 | name: "transferFrom", 71 | outputs: [ 72 | { 73 | name: "", 74 | type: "bool", 75 | }, 76 | ], 77 | payable: false, 78 | stateMutability: "nonpayable", 79 | type: "function", 80 | }, 81 | { 82 | constant: false, 83 | inputs: [ 84 | { 85 | name: "wad", 86 | type: "uint256", 87 | }, 88 | ], 89 | name: "withdraw", 90 | outputs: [], 91 | payable: false, 92 | stateMutability: "nonpayable", 93 | type: "function", 94 | }, 95 | { 96 | constant: true, 97 | inputs: [], 98 | name: "decimals", 99 | outputs: [ 100 | { 101 | name: "", 102 | type: "uint8", 103 | }, 104 | ], 105 | payable: false, 106 | stateMutability: "view", 107 | type: "function", 108 | }, 109 | { 110 | constant: true, 111 | inputs: [ 112 | { 113 | name: "", 114 | type: "address", 115 | }, 116 | ], 117 | name: "balanceOf", 118 | outputs: [ 119 | { 120 | name: "", 121 | type: "uint256", 122 | }, 123 | ], 124 | payable: false, 125 | stateMutability: "view", 126 | type: "function", 127 | }, 128 | { 129 | constant: true, 130 | inputs: [], 131 | name: "symbol", 132 | outputs: [ 133 | { 134 | name: "", 135 | type: "string", 136 | }, 137 | ], 138 | payable: false, 139 | stateMutability: "view", 140 | type: "function", 141 | }, 142 | { 143 | constant: false, 144 | inputs: [ 145 | { 146 | name: "dst", 147 | type: "address", 148 | }, 149 | { 150 | name: "wad", 151 | type: "uint256", 152 | }, 153 | ], 154 | name: "transfer", 155 | outputs: [ 156 | { 157 | name: "", 158 | type: "bool", 159 | }, 160 | ], 161 | payable: false, 162 | stateMutability: "nonpayable", 163 | type: "function", 164 | }, 165 | { 166 | constant: false, 167 | inputs: [], 168 | name: "deposit", 169 | outputs: [], 170 | payable: true, 171 | stateMutability: "payable", 172 | type: "function", 173 | }, 174 | { 175 | constant: true, 176 | inputs: [ 177 | { 178 | name: "", 179 | type: "address", 180 | }, 181 | { 182 | name: "", 183 | type: "address", 184 | }, 185 | ], 186 | name: "allowance", 187 | outputs: [ 188 | { 189 | name: "", 190 | type: "uint256", 191 | }, 192 | ], 193 | payable: false, 194 | stateMutability: "view", 195 | type: "function", 196 | }, 197 | { 198 | payable: true, 199 | stateMutability: "payable", 200 | type: "fallback", 201 | }, 202 | { 203 | anonymous: false, 204 | inputs: [ 205 | { 206 | indexed: true, 207 | name: "src", 208 | type: "address", 209 | }, 210 | { 211 | indexed: true, 212 | name: "guy", 213 | type: "address", 214 | }, 215 | { 216 | indexed: false, 217 | name: "wad", 218 | type: "uint256", 219 | }, 220 | ], 221 | name: "Approval", 222 | type: "event", 223 | }, 224 | { 225 | anonymous: false, 226 | inputs: [ 227 | { 228 | indexed: true, 229 | name: "src", 230 | type: "address", 231 | }, 232 | { 233 | indexed: true, 234 | name: "dst", 235 | type: "address", 236 | }, 237 | { 238 | indexed: false, 239 | name: "wad", 240 | type: "uint256", 241 | }, 242 | ], 243 | name: "Transfer", 244 | type: "event", 245 | }, 246 | { 247 | anonymous: false, 248 | inputs: [ 249 | { 250 | indexed: true, 251 | name: "dst", 252 | type: "address", 253 | }, 254 | { 255 | indexed: false, 256 | name: "wad", 257 | type: "uint256", 258 | }, 259 | ], 260 | name: "Deposit", 261 | type: "event", 262 | }, 263 | { 264 | anonymous: false, 265 | inputs: [ 266 | { 267 | indexed: true, 268 | name: "src", 269 | type: "address", 270 | }, 271 | { 272 | indexed: false, 273 | name: "wad", 274 | type: "uint256", 275 | }, 276 | ], 277 | name: "Withdrawal", 278 | type: "event", 279 | }, 280 | ], 281 | }; 282 | -------------------------------------------------------------------------------- /test/projects/forked/contracts/ContractA.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: UNLICENSED */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | 5 | contract ContractA { 6 | uint x; 7 | 8 | function sendFn() public { 9 | x = 1; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/projects/forked/contracts/WETH.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: UNLICENSED */ 2 | /** 3 | *Submitted for verification at Etherscan.io on 2017-12-12 4 | */ 5 | 6 | // Copyright (C) 2015, 2016, 2017 Dapphub 7 | 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU General Public License for more details. 17 | 18 | // You should have received a copy of the GNU General Public License 19 | // along with this program. If not, see . 20 | 21 | pragma solidity >=0.8.0 <0.9.0; 22 | 23 | contract WETH9 { 24 | string public name = "Wrapped Ether"; 25 | string public symbol = "WETH"; 26 | uint8 public decimals = 18; 27 | 28 | event Approval(address indexed src, address indexed guy, uint wad); 29 | event Transfer(address indexed src, address indexed dst, uint wad); 30 | event Deposit(address indexed dst, uint wad); 31 | event Withdrawal(address indexed src, uint wad); 32 | 33 | mapping (address => uint) public balanceOf; 34 | mapping (address => mapping (address => uint)) public allowance; 35 | 36 | receive() external payable { 37 | deposit(); 38 | } 39 | function deposit() public payable { 40 | balanceOf[msg.sender] += msg.value; 41 | emit Deposit(msg.sender, msg.value); 42 | } 43 | function withdraw(uint wad) public { 44 | require(balanceOf[msg.sender] >= wad); 45 | balanceOf[msg.sender] -= wad; 46 | payable(msg.sender).transfer(wad); 47 | emit Withdrawal(msg.sender, wad); 48 | } 49 | 50 | function totalSupply() public view returns (uint) { 51 | return address(this).balance; 52 | } 53 | 54 | function approve(address guy, uint wad) public returns (bool) { 55 | allowance[msg.sender][guy] = wad; 56 | emit Approval(msg.sender, guy, wad); 57 | return true; 58 | } 59 | 60 | function transfer(address dst, uint wad) public returns (bool) { 61 | return transferFrom(msg.sender, dst, wad); 62 | } 63 | 64 | function transferFrom(address src, address dst, uint wad) 65 | public 66 | returns (bool) 67 | { 68 | require(balanceOf[src] >= wad); 69 | 70 | if (src != msg.sender ) { 71 | require(allowance[src][msg.sender] >= wad); 72 | allowance[src][msg.sender] -= wad; 73 | } 74 | 75 | balanceOf[src] -= wad; 76 | balanceOf[dst] += wad; 77 | 78 | emit Transfer(src, dst, wad); 79 | 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/projects/forked/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import "@nomicfoundation/hardhat-ethers"; 3 | import { HardhatUserConfig } from "hardhat/types"; 4 | 5 | // We load the plugin here. 6 | import "../../../src/index"; 7 | 8 | import { ABI } from "./abi"; 9 | 10 | if (process.env.ALCHEMY_TOKEN === undefined) { 11 | throw new Error("Forked hardhat test requires ALCHEMY_TOKEN set in env"); 12 | } 13 | 14 | const config: HardhatUserConfig = { 15 | solidity: "0.8.17", 16 | networks: { 17 | hardhat: { 18 | forking: { 19 | url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_TOKEN!}`, 20 | }, 21 | }, 22 | }, 23 | mocha: { 24 | timeout: 100000, 25 | reporter: "dot" 26 | }, 27 | gasReporter: { 28 | remoteContracts: [ 29 | { 30 | name: "WETH", 31 | address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 32 | abi: ABI.wethABI, 33 | }, 34 | ], 35 | }, 36 | }; 37 | 38 | // eslint-disable-next-line import/no-default-export 39 | export default config; 40 | -------------------------------------------------------------------------------- /test/projects/forked/test/reset.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { Contract } from "ethers"; 3 | import { network, ethers } from "hardhat"; 4 | 5 | describe("contractA", function() { 6 | let instance: Contract; 7 | let startBlockNumber: number; 8 | 9 | before(async () => { 10 | startBlockNumber = await ethers.provider.getBlockNumber(); 11 | }); 12 | beforeEach(async () => { 13 | await network.provider.request({ 14 | method: "hardhat_reset", 15 | params: [ 16 | { 17 | forking: { 18 | jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_TOKEN!}`, 19 | blockNumber: startBlockNumber, 20 | }, 21 | }, 22 | ], 23 | }); 24 | 25 | const factory = await ethers.getContractFactory("ContractA"); 26 | instance = await factory.deploy(); 27 | }); 28 | 29 | it('sends', async function(){ 30 | await instance.sendFn(); 31 | }); 32 | 33 | it('sends again', async function(){ 34 | await instance.sendFn(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/projects/forked/test/weth.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | describe("WETH contract", function () { 4 | it("should deposit weth", async function () { 5 | const WETH = await ethers.getContractAt( 6 | "WETH9", 7 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 8 | ); 9 | 10 | await WETH.deposit({ value: ethers.parseEther("1.0") }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/projects/merge/.gitignore: -------------------------------------------------------------------------------- 1 | gasReporterOutput.json 2 | -------------------------------------------------------------------------------- /test/projects/merge/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/types"; 2 | 3 | // We load the plugin here. 4 | import "../../../src/index"; 5 | 6 | const config: HardhatUserConfig = { 7 | solidity: "0.5.8", 8 | mocha: { 9 | timeout: 10_000 10 | } 11 | }; 12 | 13 | // eslint-disable-next-line import/no-default-export 14 | export default config; 15 | -------------------------------------------------------------------------------- /test/projects/merge/malformatted-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "ethGasReporter", 3 | "config": { 4 | "blockLimit": 12000000, 5 | "defaultGasPrice": 5, 6 | "currency": "USD", 7 | "coinmarketcap": "", 8 | "ethPrice": null, 9 | "gasPrice": 20, 10 | "outputFile": "", 11 | "rst": false, 12 | "rstTitle": "", 13 | "showTimeSpent": true, 14 | "srcPath": "contracts", 15 | "artifactType": "truffle-v5", 16 | "proxyResolver": null, 17 | "metadata": { 18 | "compiler": { 19 | "version": "0.4.25" 20 | }, 21 | "settings": { 22 | "optimizer": { 23 | "enabled": false 24 | } 25 | } 26 | }, 27 | "showMethodSig": false, 28 | "maxMethodDiff": 25, 29 | "excludeContracts": [], 30 | "onlyCalledMethods": true, 31 | "url": "http://localhost:8545" 32 | }, 33 | "info": { 34 | "methods": { 35 | "AddressResolver_51456061": { 36 | "key": "51456061", 37 | "contract": "AddressResolver", 38 | "method": "getSynth", 39 | "fnSig": "getSynth(bytes32)", 40 | "gasData": [ 41 | 25891 42 | ], 43 | "numberOfCalls": 1 44 | }, 45 | "AddressResolver_79ba5097": { 46 | "key": "79ba5097", 47 | "contract": "AddressResolver", 48 | "method": "acceptOwnership", 49 | "fnSig": "acceptOwnership()", 50 | "gasData": [], 51 | "numberOfCalls": 0 52 | }, 53 | "AddressResolver_9f42102f": { 54 | "key": "9f42102f", 55 | "contract": "AddressResolver", 56 | "method": "areAddressesImported", 57 | "fnSig": "areAddressesImported(bytes32[],address[])", 58 | "gasData": [ 59 | 9337, 60 | 21873 61 | ], 62 | "numberOfCalls": 2 63 | } 64 | }, 65 | "deployments": [ 66 | { 67 | "name": "@chainlink/contracts-0.0.10/src/v0.5/interfaces/AggregatorV2V3Interface.sol:AggregatorV2V3Interface", 68 | "bytecode": "0x", 69 | "deployedBytecode": "0x", 70 | "gasData": [ 71 | 7287989, 72 | 7288085, 73 | 7288085, 74 | 7288085, 75 | 7288085 76 | ] 77 | }, 78 | { 79 | "name": "@chainlink/contracts-0.0.10/src/v0.5/interfaces/FlagsInterface.sol:FlagsInterface", 80 | "bytecode": "0x", 81 | "deployedBytecode": "0x", 82 | "gasData": [] 83 | }, 84 | { 85 | "name": "Address", 86 | "bytecode": "0x60556023600b82828239805160001a607314601657fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a723158203dcdf84dd61be7f2cf50e1d67c9700ec52d32e98e1872b24b4879d2b4a8db89364736f6c63430005100032", 87 | "deployedBytecode": "0x73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a723158203dcdf84dd61be7f2cf50e1d67c9700ec52d32e98e1872b24b4879d2b4a8db89364736f6c63430005100032", 88 | "gasData": [ 89 | 301239, 90 | 903314 91 | ] 92 | } 93 | ], 94 | "blockLimit": 12000000 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/projects/merge/malformatted-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "ethGasReporter", 3 | "config": { 4 | "blockLimit": 12000000, 5 | "defaultGasPrice": 5, 6 | "currency": "USD", 7 | "coinmarketcap": "", 8 | "ethPrice": null, 9 | "gasPrice": 20, 10 | "outputFile": "", 11 | "rst": false, 12 | "rstTitle": "", 13 | "showTimeSpent": true, 14 | "srcPath": "contracts", 15 | "artifactType": "truffle-v5", 16 | "proxyResolver": null, 17 | "metadata": { 18 | "compiler": { 19 | "version": "0.4.25" 20 | }, 21 | "settings": { 22 | "optimizer": { 23 | "enabled": false 24 | } 25 | } 26 | }, 27 | "showMethodSig": false, 28 | "maxMethodDiff": 25, 29 | "excludeContracts": [], 30 | "onlyCalledMethods": true, 31 | "url": "http://localhost:8545" 32 | }, 33 | "info": { 34 | "methods": { 35 | "AddressResolver_51456061": { 36 | "key": "51456061", 37 | "contract": "AddressResolver", 38 | "method": "getSynth", 39 | "fnSig": "getSynth(bytes32)", 40 | "gasData": [], 41 | "numberOfCalls": 0 42 | }, 43 | "AddressResolver_79ba5097": { 44 | "key": "79ba5097", 45 | "contract": "AddressResolver", 46 | "method": "acceptOwnership", 47 | "fnSig": "acceptOwnership()", 48 | "gasData": [ 49 | 12845, 50 | 21334 51 | ], 52 | "numberOfCalls": 2 53 | }, 54 | "AddressResolver_9f42102f": { 55 | "key": "9f42102f", 56 | "contract": "AddressResolver", 57 | "method": "areAddressesImported", 58 | "fnSig": "areAddressesImported(bytes32[],address[])", 59 | "gasData": [ 60 | 14008 61 | ], 62 | "numberOfCalls": 1 63 | } 64 | }, 65 | "deployments": [ 66 | { 67 | "name": "@chainlink/contracts-0.0.10/src/v0.5/interfaces/AggregatorV2V3Interface.sol:AggregatorV2V3Interface", 68 | "bytecode": "0x", 69 | "deployedBytecode": "0x", 70 | "gasData": [] 71 | }, 72 | { 73 | "name": "@chainlink/contracts-0.0.10/src/v0.5/interfaces/FlagsInterface.sol:FlagsInterface", 74 | "bytecode": "0x", 75 | "deployedBytecode": "0x", 76 | "gasData": [ 77 | 9123041, 78 | 9123042 79 | ] 80 | }, 81 | { 82 | "name": "Address", 83 | "bytecode": "0x60556023600b82828239805160001a607314601657fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a723158203dcdf84dd61be7f2cf50e1d67c9700ec52d32e98e1872b24b4879d2b4a8db89364736f6c63430005100032", 84 | "deployedBytecode": "0x73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a723158203dcdf84dd61be7f2cf50e1d67c9700ec52d32e98e1872b24b4879d2b4a8db89364736f6c63430005100032", 85 | "gasData": [ 86 | 459817, 87 | 1459817 88 | ] 89 | } 90 | ], 91 | "blockLimit": 12000000 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/projects/options/contracts/DuplicateA.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract Duplicate { 5 | uint x; 6 | uint y; 7 | 8 | function a() public { 9 | x = 5; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/projects/options/contracts/DuplicateB.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract Duplicate { 5 | uint x; 6 | uint y; 7 | 8 | function b() public { 9 | x = 5; 10 | y = 5; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/projects/options/contracts/EtherRouter/EtherRouter.sol: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of The Colony Network. 3 | The Colony Network is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | The Colony Network is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | You should have received a copy of the GNU General Public License 12 | along with The Colony Network. If not, see . 13 | */ 14 | 15 | /* SPDX-License-Identifier: MIT */ 16 | pragma solidity >=0.8.0 <0.9.0; 17 | 18 | import "./Resolver.sol"; 19 | 20 | contract EtherRouter { 21 | Resolver public resolver; 22 | 23 | fallback() external payable { 24 | if (msg.sig == 0) { 25 | return; 26 | } 27 | // Contracts that want to receive Ether with a plain "send" have to implement 28 | // a fallback function with the payable modifier. Contracts now throw if no payable 29 | // fallback function is defined and no function matches the signature. 30 | // However, 'send' only provides 2300 gas, which is not enough for EtherRouter 31 | // so we shortcut it here. 32 | // 33 | // Note that this means we can never have a fallback function that 'does' stuff. 34 | // but those only really seem to be ICOs, to date. To be explicit, there is a hard 35 | // decision to be made here. Either: 36 | // 1. Contracts that use 'send' or 'transfer' cannot send money to Colonies/ColonyNetwork 37 | // 2. We commit to never using a fallback function that does anything. 38 | // 39 | // We have decided on option 2 here. In the future, if we wish to have such a fallback function 40 | // for a Colony, it could be in a separate extension contract. 41 | 42 | // Get routing information for the called function 43 | address destination = resolver.lookup(msg.sig); 44 | 45 | // Make the call 46 | assembly { 47 | let size := extcodesize(destination) 48 | if eq(size, 0) { revert(0,0) } 49 | 50 | calldatacopy(mload(0x40), 0, calldatasize()) 51 | let result := delegatecall(gas(), destination, mload(0x40), calldatasize(), mload(0x40), 0) // ignore-swc-112 calls are only to trusted contracts 52 | // as their addresses are controlled by the Resolver which we trust 53 | returndatacopy(mload(0x40), 0, returndatasize()) 54 | switch result 55 | case 1 { return(mload(0x40), returndatasize()) } // ignore-swc-113 56 | default { revert(mload(0x40), returndatasize()) } 57 | } 58 | } 59 | 60 | function setResolver(address _resolver) public 61 | { 62 | resolver = Resolver(_resolver); 63 | } 64 | 65 | receive() external payable {} 66 | } 67 | 68 | -------------------------------------------------------------------------------- /test/projects/options/contracts/EtherRouter/Factory.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "./VersionA.sol"; 5 | import "./VersionB.sol"; 6 | 7 | contract Factory { 8 | 9 | VersionA public versionA; 10 | VersionB public versionB; 11 | 12 | function deployVersionA() public { 13 | versionA = new VersionA(); 14 | } 15 | 16 | function deployVersionB() public { 17 | versionB = new VersionB(); 18 | } 19 | } -------------------------------------------------------------------------------- /test/projects/options/contracts/EtherRouter/Resolver.sol: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of The Colony Network. 3 | The Colony Network is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | The Colony Network is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | You should have received a copy of the GNU General Public License 12 | along with The Colony Network. If not, see . 13 | */ 14 | 15 | /* SPDX-License-Identifier: MIT */ 16 | pragma solidity >=0.8.0 <0.9.0; 17 | 18 | contract Resolver { 19 | mapping (bytes4 => address) public pointers; 20 | 21 | function register(string memory signature, address destination) public 22 | { 23 | pointers[stringToSig(signature)] = destination; 24 | } 25 | 26 | function lookup(bytes4 sig) public view returns(address) { 27 | return pointers[sig]; 28 | } 29 | 30 | function stringToSig(string memory signature) public pure returns(bytes4) { 31 | return bytes4(keccak256(abi.encodePacked(signature))); 32 | } 33 | } -------------------------------------------------------------------------------- /test/projects/options/contracts/EtherRouter/VersionA.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract VersionA { 5 | function setValue() public { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/projects/options/contracts/EtherRouter/VersionB.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract VersionB { 5 | function setValue() public { 6 | } 7 | 8 | function setAnotherValue() public { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/projects/options/contracts/Immutable.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract Immutable { 5 | uint public val; 6 | address public immutable owner; 7 | uint public immutable scalar; 8 | 9 | constructor(uint base) { 10 | uint _scalar = base + 5; 11 | scalar = _scalar; 12 | owner = msg.sender; 13 | } 14 | 15 | function setVal(uint x) public { 16 | val = x* scalar; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/projects/options/contracts/MultiContractFile.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract MultiContractFileA { 5 | uint x; 6 | 7 | function hello() public { 8 | x = 5; 9 | } 10 | } 11 | 12 | contract MultiContractFileB { 13 | uint x; 14 | 15 | function goodbye() public { 16 | x = 5; 17 | } 18 | } -------------------------------------------------------------------------------- /test/projects/options/contracts/Undeployed.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract Undeployed { 5 | event Amount(uint val); 6 | 7 | function f() public { 8 | emit Amount(5); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/projects/options/contracts/VariableConstructor.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "./VariableCosts.sol"; 5 | 6 | contract VariableConstructor is VariableCosts { 7 | string name; 8 | constructor(string memory _name) { 9 | name = _name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/projects/options/contracts/VariableCosts.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "./Wallets/Wallet.sol"; 5 | import "./MultiContractFile.sol"; 6 | 7 | contract VariableCosts is Wallet { 8 | uint q; 9 | mapping(uint => address) map; 10 | MultiContractFileA multi; 11 | 12 | constructor() { 13 | multi = new MultiContractFileA(); 14 | } 15 | 16 | function callEmptyFn() public pure {} 17 | 18 | function callPureFnReturn(uint x) public pure returns (uint){ 19 | return x; 20 | } 21 | 22 | function callRevertingPureFn() public pure { 23 | require(false, "no"); 24 | } 25 | 26 | function callViewFn(uint x) public view returns (address){ 27 | return map[x]; 28 | } 29 | 30 | function addToMap(uint[] memory adds) public { 31 | for(uint i = 0; i < adds.length; i++) 32 | map[adds[i]] = address(this); 33 | } 34 | 35 | function removeFromMap(uint[] memory dels) public { 36 | for(uint i = 0; i < dels.length; i++) 37 | delete map[dels[i]]; 38 | } 39 | 40 | function unusedMethod(address a) public { 41 | map[1000] = a; 42 | } 43 | 44 | function methodThatThrows(bool err) public { 45 | require(!err); 46 | q = 5; 47 | } 48 | 49 | function otherContractMethod() public { 50 | multi.hello(); // 20,000 gas (sets uint to 5 from zero) 51 | multi.hello(); // 5,000 gas (sets existing storage) 52 | multi.hello(); // 5,000 gas (sets existing storage) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/projects/options/contracts/Wallets/Wallet.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract Wallet { 5 | 6 | event Deposit(address indexed _sender, uint _value); 7 | 8 | function transferPayment(uint payment, address payable recipient) public { 9 | payable(recipient).transfer(payment); 10 | } 11 | 12 | function sendPayment(uint payment, address payable recipient) public { 13 | if (!payable(recipient).send(payment)) 14 | revert(); 15 | } 16 | 17 | function getBalance() public view returns(uint){ 18 | return address(this).balance; 19 | } 20 | 21 | receive() external payable 22 | { 23 | if (msg.value > 0) 24 | emit Deposit(msg.sender, msg.value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.default.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import "@nomicfoundation/hardhat-ethers"; 3 | import { HardhatUserConfig } from "hardhat/types"; 4 | 5 | // We load the plugin here. 6 | import "../../../src/index"; 7 | 8 | const config: HardhatUserConfig = { 9 | solidity: "0.8.19", 10 | mocha: { 11 | reporter: "dot" 12 | } 13 | }; 14 | 15 | // eslint-disable-next-line import/no-default-export 16 | export default config; 17 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.options.a.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TESTS: 3 | * + Complex compiler details 4 | * + Non-ethereum L1 (polygon) with live market price 5 | * + Custom gasPrice API call 6 | * + Exclude contracts from reporting 7 | * + Display full method signature 8 | * + Dark mode 9 | * + RST titles 10 | */ 11 | 12 | // eslint-disable-next-line import/no-extraneous-dependencies 13 | import "@nomicfoundation/hardhat-ethers"; 14 | import { HardhatUserConfig } from "hardhat/types"; 15 | import { EtherRouterResolver } from "../../../src/lib/resolvers/etherrouter"; 16 | 17 | // We load the plugin here. 18 | import "../../../src/index"; 19 | 20 | const config: HardhatUserConfig = { 21 | solidity: { 22 | version: "0.8.24", 23 | settings: { 24 | optimizer: { 25 | enabled: true, 26 | runs: 10_000 27 | }, 28 | viaIR: true, 29 | evmVersion: "shanghai" 30 | } 31 | }, 32 | mocha: { 33 | reporter: 'dot' 34 | }, 35 | gasReporter: { 36 | currency: "CHF", 37 | L1: "ethereum", 38 | etherscan: process.env.ETHERSCAN_API_KEY, 39 | coinmarketcap: process.env.CMC_API_KEY, 40 | rst: true, 41 | rstTitle: "Ethereum Report", 42 | excludeContracts: ["EtherRouter/EtherRouter.sol"], 43 | showMethodSig: true, 44 | enabled: true, 45 | darkMode: true, 46 | proxyResolver: new EtherRouterResolver(), 47 | includeBytecodeInJSON: true, 48 | trackGasDeltas: true 49 | } 50 | }; 51 | 52 | // eslint-disable-next-line import/no-default-export 53 | export default config; 54 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.options.b.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TESTS: 3 | * + user-configured token and gasPrice 4 | * + write-to-custom-file-name (JSON & txt) 5 | * + force terminal output w/ custom output 6 | * + show uncalled methods 7 | */ 8 | 9 | // eslint-disable-next-line import/no-extraneous-dependencies 10 | import "@nomicfoundation/hardhat-ethers"; 11 | import { HardhatUserConfig } from "hardhat/types"; 12 | 13 | // We load the plugin here. 14 | import "../../../src/index"; 15 | 16 | const config: HardhatUserConfig = { 17 | solidity: { 18 | version: "0.8.24", 19 | settings: { 20 | optimizer: { 21 | enabled: true, 22 | runs: 10_000 23 | }, 24 | viaIR: true, 25 | evmVersion: "shanghai" 26 | } 27 | }, 28 | mocha: { 29 | reporter: 'dot' 30 | }, 31 | gasReporter: { 32 | enabled: true, 33 | token: "ETC", 34 | tokenPrice: "200.00", 35 | gasPrice: 40, 36 | showUncalledMethods: true, 37 | includeIntrinsicGas: false, 38 | outputFile: "./testGasReport.txt", 39 | outputJSONFile: "./gas.json", 40 | forceTerminalOutput: true, 41 | forceTerminalOutputFormat: 'legacy' 42 | } 43 | }; 44 | 45 | // eslint-disable-next-line import/no-default-export 46 | export default config; 47 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.options.c.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TESTS: 3 | * + Markdown format 4 | * + L2: Optimism 5 | * + Live market prices in CHF 6 | * + User configured gasPrice and baseFee 7 | */ 8 | // eslint-disable-next-line import/no-extraneous-dependencies 9 | import "@nomicfoundation/hardhat-ethers"; 10 | import { HardhatUserConfig } from "hardhat/types"; 11 | 12 | // We load the plugin here. 13 | import "../../../src/index"; 14 | 15 | const config: HardhatUserConfig = { 16 | solidity: "0.8.24", 17 | mocha: { 18 | reporter: 'dot' 19 | }, 20 | gasReporter: { 21 | currency: "CHF", 22 | token: "ETH", 23 | coinmarketcap: process.env.CMC_API_KEY, 24 | L2: "optimism", 25 | gasPrice: 0.098775564, 26 | baseFee: 79, 27 | blobBaseFee: 15, 28 | reportFormat: "markdown", 29 | enabled: true 30 | } 31 | }; 32 | 33 | // eslint-disable-next-line import/no-default-export 34 | export default config; 35 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.options.e.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TESTS: 3 | * + Default Terminal Format 4 | * + L2: Optimism 5 | * + reportPureAndViewMethods 6 | * + excludeAutoGeneratedGetters 7 | */ 8 | // eslint-disable-next-line import/no-extraneous-dependencies 9 | import "@nomicfoundation/hardhat-ethers"; 10 | import { HardhatUserConfig } from "hardhat/types"; 11 | 12 | // We load the plugin here. 13 | import "../../../src/index"; 14 | 15 | const config: HardhatUserConfig = { 16 | solidity: "0.8.24", 17 | mocha: { 18 | reporter: 'dot' 19 | }, 20 | gasReporter: { 21 | coinmarketcap: process.env.CMC_API_KEY, 22 | L2: "optimism", 23 | etherscan: process.env.ETHERSCAN_API_KEY, 24 | enabled: true, 25 | reportPureAndViewMethods: true, 26 | excludeAutoGeneratedGetters: true, 27 | currencyDisplayPrecision: 7, 28 | } 29 | }; 30 | 31 | // eslint-disable-next-line import/no-default-export 32 | export default config; 33 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.options.f.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TESTS: 3 | * + Default Terminal Format 4 | * + L2: Base 5 | * + reportPureAndViewMethods 6 | * + excludeAutoGeneratedGetters 7 | */ 8 | // eslint-disable-next-line import/no-extraneous-dependencies 9 | import "@nomicfoundation/hardhat-ethers"; 10 | import { HardhatUserConfig } from "hardhat/types"; 11 | 12 | // We load the plugin here. 13 | import "../../../src/index"; 14 | 15 | const config: HardhatUserConfig = { 16 | solidity: "0.8.24", 17 | mocha: { 18 | reporter: 'dot' 19 | }, 20 | gasReporter: { 21 | coinmarketcap: process.env.CMC_API_KEY, 22 | L2: "base", 23 | etherscan: process.env.ETHERSCAN_API_KEY, 24 | enabled: true, 25 | reportPureAndViewMethods: true, 26 | excludeAutoGeneratedGetters: true, 27 | currencyDisplayPrecision: 7, 28 | } 29 | }; 30 | 31 | // eslint-disable-next-line import/no-default-export 32 | export default config; 33 | -------------------------------------------------------------------------------- /test/projects/options/hardhat.options.g.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TESTS: 3 | * + Default Terminal Format 4 | * + L2: Arbitrum 5 | * + reportPureAndViewMethods 6 | * + excludeAutoGeneratedGetters 7 | */ 8 | // eslint-disable-next-line import/no-extraneous-dependencies 9 | import "@nomicfoundation/hardhat-ethers"; 10 | import { HardhatUserConfig } from "hardhat/types"; 11 | 12 | // We load the plugin here. 13 | import "../../../src/index"; 14 | 15 | const config: HardhatUserConfig = { 16 | networks: { 17 | hardhat: { 18 | gasPrice: 100_000_000_000, // ~Order of mag > than necessary 19 | }, 20 | }, 21 | solidity: "0.8.24", 22 | mocha: { 23 | reporter: 'dot' 24 | }, 25 | gasReporter: { 26 | coinmarketcap: process.env.CMC_API_KEY, 27 | currencyDisplayPrecision: 4, 28 | L2: "arbitrum", 29 | 30 | // DEPRECATED ETHERSCAN V1 31 | L1Etherscan: process.env.ETHERSCAN_API_KEY, 32 | L2Etherscan: process.env.ARBITRUM_API_KEY, 33 | 34 | enabled: true, 35 | reportPureAndViewMethods: true, 36 | excludeAutoGeneratedGetters: true 37 | } 38 | }; 39 | 40 | // eslint-disable-next-line import/no-default-export 41 | export default config; 42 | -------------------------------------------------------------------------------- /test/projects/options/test/duplicatenames.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | describe("Duplicate contract names", function() { 4 | let DuplicateA: any; 5 | let DuplicateB: any; 6 | 7 | before(async function(){ 8 | DuplicateA = await ethers.getContractFactory("contracts/DuplicateA.sol:Duplicate"); 9 | DuplicateB = await ethers.getContractFactory("contracts/DuplicateB.sol:Duplicate"); 10 | }); 11 | 12 | it("a", async function() { 13 | const d = await DuplicateA.deploy(); 14 | await d.a(); 15 | }); 16 | 17 | it("b", async function() { 18 | const d = await DuplicateB.deploy(); 19 | await d.b(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/projects/options/test/etherrouter.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { Contract } from "ethers"; 3 | import { ethers } from "hardhat"; 4 | 5 | describe("EtherRouter Proxy", function() { 6 | let Router; 7 | let Resolver; 8 | let Factory; 9 | let VersionA; 10 | let VersionB; 11 | let router: Contract; 12 | let resolver: Contract; 13 | let factory: Contract; 14 | let versionA: Contract; 15 | let versionB: Contract; 16 | 17 | before(async function() { 18 | Router = await ethers.getContractFactory("EtherRouter"); 19 | Resolver = await ethers.getContractFactory("Resolver"); 20 | Factory = await ethers.getContractFactory("Factory"); 21 | VersionA = await ethers.getContractFactory("VersionA"); 22 | VersionB = await ethers.getContractFactory("VersionB"); 23 | 24 | router = await Router.deploy(); 25 | resolver = await Resolver.deploy(); 26 | factory = await Factory.deploy(); 27 | versionA = await VersionA.deploy(); 28 | 29 | // Emulate internal deployment 30 | await factory.deployVersionB(); 31 | const versionBAddress = await factory.versionB(); 32 | versionB = VersionB.attach(versionBAddress); 33 | }); 34 | 35 | it("Resolves methods routed through an EtherRouter proxy", async function() { 36 | const options: any = {}; 37 | 38 | await router.setResolver(await resolver.getAddress()); 39 | 40 | await resolver.register("setValue()", await versionA.getAddress()); 41 | options.data = versionA.interface.encodeFunctionData("setValue"); 42 | await router.fallback!(options); 43 | 44 | await resolver.register("setValue()", await versionB.getAddress()); 45 | options.data = versionB.interface.encodeFunctionData("setValue"); 46 | await router.fallback!(options); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/projects/options/test/factory.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { Contract } from "ethers"; 3 | import { ethers } from "hardhat"; 4 | 5 | describe("Factory deployment: different contract / same method name", function () { 6 | let Factory; 7 | let VersionA; 8 | let VersionB; 9 | let factory: Contract; 10 | let versionA: Contract; 11 | let versionB: Contract; 12 | 13 | before(async function () { 14 | Factory = await ethers.getContractFactory("Factory"); 15 | VersionA = await ethers.getContractFactory("VersionA"); 16 | VersionB = await ethers.getContractFactory("VersionB"); 17 | 18 | factory = await Factory.deploy(); 19 | 20 | await factory.deployVersionA(); 21 | versionA = VersionA.attach(await factory.versionA()); 22 | 23 | await factory.deployVersionB(); 24 | versionB = VersionB.attach(await factory.versionB()); 25 | }); 26 | 27 | it("Calling both versionA.setValue and versionB.setValue", async function () { 28 | await versionA.setValue(); 29 | await versionB.setValue(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/projects/options/test/immutable.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | describe("Immutable", function() { 4 | let a: any; 5 | 6 | before(async function() { 7 | const Immutable = await ethers.getContractFactory("Immutable"); 8 | 9 | a = await Immutable.deploy(5); 10 | }); 11 | 12 | it("a and b", async function() { 13 | await a.setVal(5); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/projects/options/test/multicontract.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | describe("MultiContractFiles", function() { 4 | let a: any; 5 | let b: any; 6 | 7 | before(async function() { 8 | const MultiContractFileA = await ethers.getContractFactory("MultiContractFileA"); 9 | const MultiContractFileB = await ethers.getContractFactory("MultiContractFileB"); 10 | 11 | a = await MultiContractFileA.deploy(); 12 | b = await MultiContractFileB.deploy(); 13 | }); 14 | 15 | it("a and b", async function() { 16 | await a.hello(); 17 | await b.goodbye(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/projects/options/test/variableconstructor.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | describe("VariableConstructor", function() { 4 | let VariableConstructor: any; 5 | const short = "s"; 6 | const medium = process.env.GAS_DELTA === "true" 7 | ? "medium_length_initializer" 8 | : "medium_length_initializerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"; 9 | 10 | const long = "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_initializer"; 11 | 12 | before(async function(){ 13 | VariableConstructor = await ethers.getContractFactory("VariableConstructor"); 14 | }) 15 | 16 | it("should should initialize with a short string", async () => { 17 | await VariableConstructor.deploy(short); 18 | }); 19 | 20 | it("should should initialize with a medium length string", async () => { 21 | await VariableConstructor.deploy(medium); 22 | }); 23 | 24 | it("should should initialize with a long string", async () => { 25 | await VariableConstructor.deploy(long); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/projects/options/test/variablecosts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { assert } from "chai"; 3 | import { ethers } from "hardhat"; 4 | import { Contract } from "ethers"; 5 | 6 | describe("VariableCosts", function() { 7 | const one = [1]; 8 | const three = process.env.GAS_DELTA === "true" ? [2, 3, 4, 5, 6, 7, 8] : [2, 3, 4]; // changing gas values if required 9 | const five = [5, 6, 7, 8, 9]; 10 | let instance: Contract; 11 | let walletB: any; 12 | 13 | beforeEach(async () => { 14 | const VariableCosts = await ethers.getContractFactory("VariableCosts"); 15 | const Wallet = await ethers.getContractFactory("Wallet"); 16 | instance = await VariableCosts.deploy(); 17 | walletB = await Wallet.deploy(); 18 | }); 19 | 20 | it("can call an empty view fn", async() => { 21 | await instance.callEmptyFn(); 22 | }); 23 | 24 | it("can call a pure fn", async() => { 25 | await instance.callPureFnReturn(5); 26 | }); 27 | 28 | it("can call a reverting pure fn", async() => { 29 | try { 30 | await instance.callRevertingPureFn(); 31 | } catch(e) { /* ignore */ } 32 | }); 33 | 34 | it("can call a view fn", async() => { 35 | await instance.callViewFn(10); 36 | }); 37 | 38 | it("should add one", async () => { 39 | await instance.addToMap(one); 40 | }); 41 | 42 | it("should add three", async () => { 43 | await instance.addToMap(three); 44 | }); 45 | 46 | it("should add even 5!", async () => { 47 | await instance.addToMap(five); 48 | }); 49 | 50 | it("should delete one", async () => { 51 | await instance.removeFromMap(one); 52 | }); 53 | 54 | it("should delete three", async () => { 55 | await instance.removeFromMap(three); 56 | }); 57 | 58 | it("should delete five", async () => { 59 | await instance.removeFromMap(five); 60 | }); 61 | 62 | it("should add five and delete one", async () => { 63 | await instance.addToMap(five); 64 | await instance.removeFromMap(one); 65 | }); 66 | 67 | it("methods that do not throw", async () => { 68 | await instance.methodThatThrows(false); 69 | }); 70 | 71 | it("methods that throw", async () => { 72 | try { 73 | await instance.methodThatThrows(true); 74 | } catch (e) {} 75 | }); 76 | 77 | it("methods that call methods in other contracts", async () => { 78 | await instance.otherContractMethod(); 79 | }); 80 | 81 | // VariableCosts is Wallet. We also have Wallet tests. So we should see 82 | // separate entries for `sendPayment` / `transferPayment` under VariableCosts 83 | // and Wallet in the report 84 | it("should allow contracts to have identically named methods", async () => { 85 | await instance.fallback!({ 86 | value: 100, 87 | }); 88 | await instance.sendPayment(50, await walletB.getAddress(), { 89 | // from: accounts[0].address 90 | }); 91 | await instance.transferPayment(50, await walletB.getAddress(), { 92 | // from: accounts[0].address 93 | }); 94 | const balance = await walletB.getBalance(); 95 | assert.equal(parseInt(balance.toString()), 100); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/projects/options/test/wallet.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { assert } from "chai"; 3 | import { Contract } from "ethers"; 4 | import { ethers } from "hardhat"; 5 | 6 | describe("Wallet", function() { 7 | let walletA: Contract; 8 | let walletB: Contract; 9 | 10 | before(async function() { 11 | const Wallet = await ethers.getContractFactory("Wallet"); 12 | walletA = await Wallet.deploy(); 13 | walletB = await Wallet.deploy(); 14 | }); 15 | 16 | it("should allow transfers and sends", async () => { 17 | await walletA.fallback!({ value: 100 }); 18 | await walletA.sendPayment(50, await walletB.getAddress()); 19 | await walletA.transferPayment(50, await walletB.getAddress()); 20 | const balance = await walletB.getBalance(); 21 | assert.equal(parseInt(balance.toString()), 100); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/projects/oz/contracts/BeaconBox.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | 6 | contract BeaconBox is Initializable { 7 | uint public beaconId; 8 | string public contents; 9 | 10 | function initialize() external initializer { } 11 | 12 | function box() public view returns (string memory) { 13 | return contents; 14 | } 15 | 16 | function setBox(string memory _contents) public { 17 | contents = _contents; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/projects/oz/contracts/BeaconBoxV2.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | 6 | contract BeaconBoxV2 is Initializable { 7 | uint public beaconId; 8 | string public contents; 9 | 10 | function initialize() external initializer { } 11 | 12 | function box() public view returns (string memory) { 13 | return contents; 14 | } 15 | 16 | function setBox(string memory _contents) public { 17 | contents = _contents; 18 | } 19 | 20 | function setBeaconId(uint _id) public { 21 | beaconId = _id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/projects/oz/contracts/ProxyBox.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | 6 | contract ProxyBox is Initializable { 7 | uint public proxyId; 8 | string public contents; 9 | 10 | function initialize() external initializer { } 11 | 12 | function box() public view returns (string memory) { 13 | return contents; 14 | } 15 | 16 | function setBox(string memory _contents) public { 17 | contents = _contents; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/projects/oz/contracts/ProxyBoxV2.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | 6 | contract ProxyBoxV2 is Initializable { 7 | uint public proxyId; 8 | string public contents; 9 | 10 | function initialize() external initializer { } 11 | 12 | function box() public view returns (string memory) { 13 | return contents; 14 | } 15 | 16 | function setBox(string memory _contents) public { 17 | contents = _contents; 18 | } 19 | 20 | function setProxyId(uint _id) public { 21 | proxyId = _id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/projects/oz/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import "@nomicfoundation/hardhat-ethers"; 3 | import '@openzeppelin/hardhat-upgrades'; 4 | import { HardhatUserConfig } from "hardhat/types"; 5 | 6 | import "../../../src/index"; 7 | 8 | const config: HardhatUserConfig = { 9 | solidity: "0.8.20", 10 | mocha: { 11 | reporter: "dot" 12 | } 13 | }; 14 | 15 | // eslint-disable-next-line import/no-default-export 16 | export default config; 17 | -------------------------------------------------------------------------------- /test/projects/oz/test/box.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from "hardhat"; 2 | 3 | // Tests adapted from OZ Hardhat plugin documentation examples 4 | describe("Box", function() { 5 | it('uses a method shadowed by an upgrade (Proxy)', async function() { 6 | const ProxyBox = await ethers.getContractFactory("ProxyBox"); 7 | const ProxyBoxV2 = await ethers.getContractFactory("ProxyBoxV2"); 8 | 9 | const instance = await upgrades.deployProxy(ProxyBox, []); 10 | await instance.setBox('hello'); 11 | 12 | const upgraded = await upgrades.upgradeProxy(await instance.getAddress(), ProxyBoxV2); 13 | await upgraded.setBox('hello again'); 14 | }); 15 | 16 | it('uses a method shadowed by an upgrade (Beacon)', async function () { 17 | const BeaconBox = await ethers.getContractFactory("BeaconBox"); 18 | const BeaconBoxV2 = await ethers.getContractFactory("BeaconBoxV2"); 19 | 20 | const beacon = await upgrades.deployBeacon(BeaconBox); 21 | const instance = await upgrades.deployBeaconProxy(beacon, BeaconBox); 22 | 23 | await instance.setBox('hello'); 24 | 25 | await upgrades.upgradeBeacon(beacon, BeaconBoxV2); 26 | const upgraded = BeaconBoxV2.attach(await instance.getAddress()); 27 | 28 | await upgraded.setBox('hello again'); 29 | await upgraded.setBeaconId(5); 30 | }); 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /test/projects/run/contracts/Base.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) 2021 Yuichiro Aoki 3 | // This test fixture adapted from https://github.com/yuichiroaoki/flash-swap-example 4 | pragma solidity =0.7.6; 5 | 6 | 7 | contract Base { 8 | 9 | bool internal locked; 10 | address public owner; 11 | 12 | event Received(address, uint); 13 | 14 | constructor() { 15 | owner = msg.sender; 16 | } 17 | modifier noReentrant() { 18 | require(!locked, "No re-entrancy"); 19 | locked = true; 20 | _; 21 | locked = false; 22 | } 23 | 24 | modifier onlyOwner() { 25 | require(owner == msg.sender, "Ownable: caller is not the owner"); 26 | _; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /test/projects/run/contracts/KyberNetworkProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Copyright (c) 2021 Yuichiro Aoki 3 | // This test fixture adapted from https://github.com/yuichiroaoki/flash-swap-example 4 | pragma solidity 0.7.6; 5 | 6 | import { IERC20 as ERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | interface KyberNetworkProxyInterface { 9 | function maxGasPrice() external view returns(uint); 10 | function getUserCapInWei(address user) external view returns(uint); 11 | function getUserCapInTokenWei(address user, ERC20 token) external view returns(uint); 12 | function enabled() external view returns(bool); 13 | function info(bytes32 id) external view returns(uint); 14 | 15 | function getExpectedRate(ERC20 src, ERC20 dest, uint srcQty) external view 16 | returns (uint expectedRate, uint slippageRate); 17 | 18 | function tradeWithHint(ERC20 src, uint srcAmount, ERC20 dest, address destAddress, uint maxDestAmount, 19 | uint minConversionRate, address walletId, bytes calldata hint) external payable returns(uint); 20 | } 21 | 22 | interface SimpleNetworkInterface { 23 | function swapTokenToToken(ERC20 src, uint srcAmount, ERC20 dest, uint minConversionRate) external returns(uint); 24 | function swapEtherToToken(ERC20 token, uint minConversionRate) external payable returns(uint); 25 | function swapTokenToEther(ERC20 token, uint srcAmount, uint minConversionRate) external returns(uint); 26 | } 27 | 28 | abstract contract KyberNetworkProxy is KyberNetworkProxyInterface, SimpleNetworkInterface {} 29 | -------------------------------------------------------------------------------- /test/projects/run/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import "@nomicfoundation/hardhat-ethers"; 3 | import { HardhatUserConfig } from "hardhat/types"; 4 | 5 | // We load the plugin here. 6 | import "../../../src/index"; 7 | 8 | if (process.env.ALCHEMY_TOKEN === undefined) { 9 | throw new Error("Forked hardhat test requires ALCHEMY_TOKEN set in env"); 10 | } 11 | 12 | const config: HardhatUserConfig = { 13 | solidity: "0.7.6", 14 | networks: { 15 | hardhat: { 16 | forking: { 17 | url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_TOKEN!}`, 18 | blockNumber: 18_000_000 19 | }, 20 | }, 21 | }, 22 | mocha: { 23 | timeout: 200_000, 24 | reporter: "dot" 25 | }, 26 | gasReporter: {}, 27 | }; 28 | 29 | // eslint-disable-next-line import/no-default-export 30 | export default config; 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/projects/run/script.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | Copyright (c) 2021 Yuichiro Aoki 4 | This test fixture adapted from https://github.com/yuichiroaoki/flash-swap-example 5 | */ 6 | 7 | /* eslint-disable import/no-extraneous-dependencies */ 8 | import type { Contract } from "ethers"; 9 | import { network, ethers } from "hardhat"; 10 | 11 | import * as IERC20 from "@openzeppelin/contracts/build/contracts/IERC20.json"; 12 | 13 | export const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" 14 | export const UNI = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984" 15 | export const DAI_WHALE = "0x60FaAe176336dAb62e284Fe19B885B095d29fB7F" 16 | export const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" 17 | 18 | export const SWAP_ROUTER = "0xE592427A0AEce92De3Edee1F18E0157C05861564" 19 | export const uniswapv3factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984" 20 | export const KYBER_ADDRESS = "0x818E6FECD516Ecc3849DAf6845e3EC868087B755" 21 | export const weth9 = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 22 | 23 | async function getErc20Balance( 24 | contract: Contract, address: string, decimals: number 25 | ): Promise { 26 | 27 | const [balance] = await Promise.all([ 28 | contract.balanceOf(address), 29 | ]) 30 | 31 | return parseFloat(ethers.formatUnits(balance, decimals)) 32 | } 33 | 34 | async function fundErc20( 35 | contract: Contract, sender: string, recepient: string, amount: string 36 | ){ 37 | 38 | const FUND_AMOUNT = ethers.parseUnits(amount, 18) 39 | 40 | // fund erc20 token to the contract 41 | const MrWhale = await ethers.getSigner(sender) 42 | 43 | const contractSigner = contract.connect(MrWhale) 44 | await (contractSigner as any).transfer(recepient, FUND_AMOUNT) 45 | } 46 | 47 | async function impersonateFundErc20( 48 | contract: Contract, sender: string, recepient: string, amount: string 49 | ) { 50 | await network.provider.request({ 51 | method: "hardhat_impersonateAccount", 52 | params: [sender], 53 | }); 54 | 55 | // fund baseToken to the contract 56 | await fundErc20(contract, sender, recepient, amount) 57 | 58 | await network.provider.request({ 59 | method: "hardhat_stopImpersonatingAccount", 60 | params: [sender], 61 | }) 62 | } 63 | 64 | async function main( 65 | borrowingTokenAddress: string, 66 | swapingPairTokenAddress: string, 67 | isUniKyb: Boolean, 68 | amount: number 69 | ) { 70 | const FlashSwaps = await ethers.getContractFactory("FlashSwaps"); 71 | const [deployer] = await ethers.getSigners(); 72 | const provider = ethers.provider; 73 | 74 | const flashswaps = await FlashSwaps.deploy( 75 | SWAP_ROUTER, 76 | uniswapv3factory, 77 | ethers.getAddress(weth9), 78 | KYBER_ADDRESS 79 | ); 80 | 81 | const DECIMALS = 18 82 | 83 | // let swapingPairToken: any; 84 | // let borrowingToken: any; 85 | // swapingPairToken = new ethers.Contract(swapingPairTokenAddress, IERC20.abi, provider) 86 | const borrowingToken = new ethers.Contract(borrowingTokenAddress, IERC20.abi, provider) 87 | 88 | await impersonateFundErc20( 89 | borrowingToken, 90 | DAI_WHALE, 91 | await flashswaps.getAddress(), 92 | "2000.0" 93 | ); 94 | 95 | const initialBalance = await getErc20Balance(borrowingToken, deployer.address, DECIMALS) 96 | console.log("deployer's initial balance", initialBalance) 97 | 98 | // borrow from token0, token1 fee1 pool 99 | await flashswaps.initFlash({ 100 | token0: ethers.getAddress(borrowingTokenAddress), // DAI 101 | token1: ethers.getAddress(USDC), 102 | token2: ethers.getAddress(swapingPairTokenAddress), // UNI 103 | fee1: 500, 104 | amount0: ethers.parseUnits(amount.toString(), DECIMALS), 105 | amount1: 0, 106 | fee2: 500, 107 | unikyb: isUniKyb, 108 | }) 109 | 110 | const endingBalance = await getErc20Balance(borrowingToken, deployer.address, DECIMALS) 111 | console.log("deployer's ending balance", endingBalance) 112 | 113 | const profit = endingBalance - initialBalance 114 | 115 | // if (profit>0) { 116 | console.log(`Congrats! You earned ${profit} DAI !!`) 117 | // } 118 | } 119 | 120 | main(DAI, UNI, false, 1500) 121 | .then(() => process.exit(0)) 122 | .catch(error => { 123 | console.error(error); 124 | process.exit(1); 125 | }); 126 | -------------------------------------------------------------------------------- /test/projects/viem/contracts/EmphaticGreeter.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "./UrGreeter.sol"; 5 | 6 | contract EmphaticGreeter is UrGreeter { 7 | string public greeting; 8 | 9 | constructor() { 10 | greeting = "!!!"; 11 | } 12 | 13 | function greet() public view returns (string memory) { 14 | return greeting; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/projects/viem/contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "./UrGreeter.sol"; 5 | 6 | contract Greeter is UrGreeter { 7 | string public greeting; 8 | 9 | constructor(string memory _greeting) { 10 | greeting = _greeting; 11 | } 12 | 13 | function greet() public view returns (string memory) { 14 | return greeting; 15 | } 16 | 17 | function setGreeting(string memory _greeting) public { 18 | greeting = _greeting; 19 | } 20 | 21 | function asOther() public {} 22 | 23 | function throwAnError(string memory message) public { 24 | greeting = "goodbye"; 25 | require(false, message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/projects/viem/contracts/UrGreeter.sol: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract UrGreeter { 5 | string public urGreeting = "squeak"; 6 | } 7 | -------------------------------------------------------------------------------- /test/projects/viem/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import "@nomicfoundation/hardhat-viem"; 3 | import { HardhatUserConfig } from "hardhat/types"; 4 | 5 | // We load the plugin here (should work even if viem is first) 6 | import "../../../src/index"; 7 | 8 | const config: HardhatUserConfig = { 9 | solidity: "0.8.19", 10 | mocha: { 11 | reporter: "dot" 12 | }, 13 | gasReporter: { 14 | reportPureAndViewMethods: true, 15 | excludeAutoGeneratedGetters: true 16 | } 17 | }; 18 | 19 | // eslint-disable-next-line import/no-default-export 20 | export default config; 21 | -------------------------------------------------------------------------------- /test/projects/viem/test/greeter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { assert, expect, use } from "chai"; 3 | import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 4 | import chaiAsPromised from "chai-as-promised"; 5 | import { viem } from "hardhat"; 6 | 7 | use(chaiAsPromised); 8 | 9 | describe("Greeter contract", function() { 10 | async function deployGreeterFixture() { 11 | const publicClient = await viem.getPublicClient(); 12 | const [owner, other] = await viem.getWalletClients(); 13 | 14 | const deployGreeter = (args: [_greeting: string]) => viem.deployContract("Greeter", args); 15 | 16 | const greeter = await deployGreeter(["Hi"]); 17 | const emphaticGreeter = await viem.deployContract("EmphaticGreeter"); 18 | 19 | const greeterAsOther = await viem.getContractAt( 20 | "Greeter", 21 | greeter.address, 22 | { client: { wallet: other } } 23 | ); 24 | 25 | return { 26 | publicClient, 27 | owner, 28 | other, 29 | deployGreeter, 30 | greeter, 31 | emphaticGreeter, 32 | greeterAsOther, 33 | } 34 | } 35 | 36 | it("Should shoud be deployable with different greetings", async function() { 37 | const { 38 | greeter, 39 | deployGreeter, 40 | } = await loadFixture(deployGreeterFixture); 41 | assert.equal(await greeter.read.greeting(), "Hi"); 42 | const greeter2 = await deployGreeter(["Hola"]); 43 | assert.equal(await greeter2.read.greeting(), "Hola"); 44 | }); 45 | 46 | it("Should return the greeting when greet is called", async function() { 47 | const { 48 | greeter, 49 | } = await loadFixture(deployGreeterFixture); 50 | assert.equal(await greeter.read.greet(), "Hi"); 51 | }); 52 | 53 | it("Should set a greeting", async function(){ 54 | const { 55 | greeter, 56 | } = await loadFixture(deployGreeterFixture); 57 | await greeter.write.setGreeting(['ciao']); 58 | assert.equal(await greeter.read.greet(), "ciao"); 59 | }) 60 | 61 | // NOTE: This test is to check whether gas reporter can catch calls from viem using 62 | // other accounts. Expected to see an entry in the method data for `asOther` 63 | it("Should call as other", async function(){ 64 | const { 65 | greeterAsOther, 66 | } = await loadFixture(deployGreeterFixture); 67 | await greeterAsOther.write.asOther(); 68 | }) 69 | 70 | it("should revert with a message", async function(){ 71 | const { 72 | greeterAsOther, 73 | } = await loadFixture(deployGreeterFixture); 74 | await expect( 75 | greeterAsOther.write.throwAnError(['throwing...']) 76 | ).to.eventually.be.rejectedWith('throwing...'); 77 | }) 78 | 79 | it("should call an inherited public state variable", async function() { 80 | const { 81 | greeter, 82 | emphaticGreeter, 83 | } = await loadFixture(deployGreeterFixture); 84 | 85 | await greeter.read.urGreeting(); 86 | await emphaticGreeter.read.urGreeting() 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/tasks/merge.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { execSync } from "child_process"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | 7 | import { TASK_GAS_REPORTER_MERGE, TASK_GAS_REPORTER_MERGE_LEGACY } from "../../src/task-names"; 8 | 9 | import { useEnvironment } from "./../helpers"; 10 | 11 | const loadJsonFile = (filepath: any) => 12 | JSON.parse(fs.readFileSync(filepath, "utf-8")); 13 | 14 | describe("Merge gasRerpoterOutput.json files task", function () { 15 | const projectPath = path.resolve( 16 | __dirname, 17 | "../projects/merge" 18 | ); 19 | 20 | const outputPath = path.resolve( 21 | __dirname, 22 | "../projects/merge/gasReporterOutput.json" 23 | ); 24 | 25 | useEnvironment(projectPath); 26 | 27 | after(() => execSync(`rm ${outputPath}`)); 28 | 29 | it("should merge gas reporter output files", async function () { 30 | const expected = loadJsonFile( 31 | path.resolve(projectPath, "mergeOutput.expected.json") 32 | ); 33 | 34 | await this.env.run(TASK_GAS_REPORTER_MERGE, { 35 | input: ["mergeOutput-*.json"], 36 | }); 37 | 38 | const result = loadJsonFile(outputPath); 39 | 40 | // Sanitize gas/price rates and other variable quantities 41 | delete result.options.coinmarketcap; 42 | delete expected.options.coinmarketcap; 43 | 44 | delete result.version; 45 | delete expected.version; 46 | 47 | delete result.options.tokenPrice; 48 | delete expected.options.tokenPrice; 49 | 50 | delete result.options.outputJSONFile; 51 | delete expected.options.outputJSONFile; 52 | 53 | delete result.data.blockLimit; 54 | delete expected.data.blockLimit; 55 | 56 | for (const key of Object.keys(result.data.methods)) { 57 | delete result.data.methods[key].cost; 58 | delete expected.data.methods[key].cost; 59 | } 60 | 61 | for (const deployment of result.data.deployments) { 62 | delete deployment.cost; 63 | } 64 | 65 | for (const deployment of expected.data.deployments) { 66 | delete deployment.cost; 67 | } 68 | 69 | assert.deepEqual(result, expected); 70 | }); 71 | 72 | it("should error on malformatted files", async function() { 73 | try { 74 | await this.env.run(TASK_GAS_REPORTER_MERGE, { 75 | input: ["malformatted-*.json"], 76 | }); 77 | assert.fail("test failed") 78 | } catch (err: any) { 79 | assert(err.message.includes("requires property \"options\"")) 80 | } 81 | }); 82 | 83 | it("should display a deprecated task warning when running legacy tasks", async function(){ 84 | await this.env.run(TASK_GAS_REPORTER_MERGE_LEGACY, { 85 | input: ["mergeOutput-*.json"], 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/unit/cases/base.ts: -------------------------------------------------------------------------------- 1 | export const cases = { 2 | // mainnet:txHash: 0xce4bfa9a3a29356b60cc85d751ad5d26e1664463582e89766ca8683e26328585 3 | ecotoneFunction_1: { 4 | tx: { 5 | "blockHash":"0x81845cc4fdf2ccefacbe2fe042c0c6f215ea8fdd67aad413bc8af60f8336b976", 6 | "blockNumber":"0xbfacd9", 7 | "from":"0xbacdbd36fef7266cd41e31a1a10af48c47de3b09", 8 | "gas":"0x16b5c", 9 | "gasPrice":"0x1234f73e", 10 | "maxFeePerGas":"0x15928ae8", 11 | "maxPriorityFeePerGas":"0x3e8", 12 | "hash":"0xce4bfa9a3a29356b60cc85d751ad5d26e1664463582e89766ca8683e26328585", 13 | "input":"0x2e7ba6ef00000000000000000000000000000000000000000000000000000000000042fc000000000000000000000000bacdbd36fef7266cd41e31a1a10af48c47de3b0900000000000000000000000000000000000000000000008f9a4288ff19c000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f033715c437d6d6f144c9f8e94cdf7fd66bbd4ee54ca4a27b39b32315b6c427662c32e2e6140a476ed124d418eb7c61b0ec3bb6465ec6417ab5a4265598378740266aa40c283849fe6d426f94e8713ac61733bcc46d9795525c4d80e3b1fe0dbf00b59a405030956af5b469afae68d512719d0b929c75e5b0e019b5a4c1baee2c6dda9e7793868c4c49e00b8c6045d23752266d96901985a8604a2f32fd4c402ae5b298275fc47ed60e7beab5f81f96d1925d19525d98883e5ed0d6c49b6ad87d3cfd03a2c839f7e92c04c229890f741155a2d264659ed25e10d4132b67f6312f9706a134125832e54366faa5601a105ae8d0771cadd2fa4334512f81fe928e3937d67983bbc7fa0a0172f3c64ec7c6f3d291747110f8263064f484b85f100212200ef4c0232be274a8f1f0fbda691ba2fe4ba36a4b0f52d3e5a77487c1f66f085d75e8a63e9a09de5fb519594a3d9103b00c71a7e930c0c40e67e3c135d93e7fd77aceb7a177b035ff29a63c33c9ec124728d666618cdad7e43a2daadd58d22e3845aec689cdb83b881b88bd6c9dad41f2693c40a5edfa28946a4efea512850ee009d484ecb849d175e476a890411b0a49ecf964a0c35ad2c0768c9008b46891549b2f07f0554d270dcfaad3f50e6c4eeaf386bdc0c54de1ea1d7c1d5eb7f54c", 14 | "nonce":"0x0", 15 | "to":"0xa2a5c549a454a1631ff226e0cf8dc4af03a61a75", 16 | "transactionIndex":"0x29", 17 | "value":"0x0", 18 | "type":"0x2", 19 | "accessList":[], 20 | "chainId":"0x2105", 21 | "v":"0x0", 22 | "r":"0xb3e2bc87354c8f5f19c9f92b0c7d51125b37535b45d4f44cbcc7f5024cf74f", 23 | "s":"0x6271faf8aec2c3e6186318514d92f3c5dd6db900cdf6264f50312e3a4e6bd10", 24 | "yParity":"0x0" 25 | }, 26 | // Etherscan 27 | l1GasUsed: 10_536, 28 | l2GasUsed: 83_649, 29 | l2GasPrice: 0.305461054, 30 | l1BlobBaseFee: 37.284000141, // read live from GasOracle @ time of retrieval 31 | l1BaseFee: 20.62228493, // actually gasPrice 32 | txFeeETH: 0.000041991074141544, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /test/unit/cases/evm.ts: -------------------------------------------------------------------------------- 1 | export const cases = { 2 | // mainnet:txHash: 0x2bff6580ceed5123bc1638d3067d87f47c03246271f5ca21bd26b5372b617768 3 | function_1: { 4 | // Etherscan 5 | gas: 156_152, 6 | gasPrice: 44.957208895, 7 | txFeeETH: 0.00702015808337204 8 | }, 9 | // mainnet:txHash: 0x4c35e6ef017f5c9622755c0e82dd0cd451b3ec95db62271075e7dd82aa2af814 10 | deployment: { 11 | // Etherscan 12 | gas: 1_940_071, 13 | gasPrice: 42.654021543, 14 | txFeeETH: 0.082751830228949553 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/gas.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { 4 | getCalldataGasForNetwork, 5 | gasToPercentOfLimit, 6 | gasToCost 7 | } from "../../src/utils/gas"; 8 | import { GasReporterOptions } from "../types"; 9 | import { 10 | BASE_ECOTONE_BASE_FEE_SCALAR, 11 | BASE_ECOTONE_BLOB_BASE_FEE_SCALAR, 12 | OPTIMISM_ECOTONE_BASE_FEE_SCALAR, 13 | OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR 14 | } from "../../src/constants"; 15 | import { cases as arbitrumCases } from "./cases/arbitrum"; 16 | import { cases as baseCases } from "./cases/base"; 17 | import { cases as optimismCases } from "./cases/optimism"; 18 | import { cases as evmCases } from "./cases/evm"; 19 | 20 | 21 | function getPercentDiff(valA: number, valB: number) { 22 | return (valA > valB) 23 | ? (valA - valB) / valA 24 | : (valB - valA) / valB; 25 | } 26 | 27 | describe("gasToPercentOfLimit", function(){ 28 | it ("calculates the percentage as a number btw 0 and 100", function() { 29 | const gas = 350_000; 30 | const limit = 700_000; 31 | 32 | const percent = gasToPercentOfLimit(gas, limit); 33 | assert.equal(percent, 50); 34 | }); 35 | }); 36 | 37 | describe("EVM L1: gasToCost", function() { 38 | const precision = 7; 39 | const options: GasReporterOptions = { 40 | tokenPrice: "1", 41 | currencyDisplayPrecision: precision 42 | } 43 | 44 | it ("calculates cost for function call", function(){ 45 | const fn = evmCases.function_1; 46 | options.gasPrice = fn.gasPrice; 47 | const cost = gasToCost(fn.gas, 0, options); 48 | 49 | assert(cost, fn.txFeeETH.toFixed(precision)); 50 | }) 51 | 52 | it ("calculates cost for deployment", function(){ 53 | const fn = evmCases.deployment; 54 | options.gasPrice = fn.gasPrice; 55 | const cost = gasToCost(fn.gas, 0, options); 56 | 57 | assert(cost, fn.txFeeETH.toFixed(precision)); 58 | }) 59 | }); 60 | 61 | describe("Optimism: getCalldataCostForNetwork", function () { 62 | const options: GasReporterOptions = { 63 | L2: "optimism", 64 | tokenPrice: "1", 65 | currencyDisplayPrecision: 8, 66 | opStackBaseFeeScalar: OPTIMISM_ECOTONE_BASE_FEE_SCALAR, 67 | opStackBlobBaseFeeScalar: OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR 68 | } 69 | 70 | it("calculates gas cost for small function call tx (bedrock)", function () { 71 | const fn = optimismCases.bedrockFunction_1; 72 | options.gasPrice = fn.l2GasPrice; 73 | options.baseFee = fn.l1BaseFee; 74 | options.optimismHardfork = "bedrock"; 75 | 76 | const gas = getCalldataGasForNetwork(options, fn.tx); 77 | const cost = gasToCost(fn.l2GasUsed, gas, options); 78 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 79 | 80 | // Actual ~ 0.72% 81 | assert(diff < .01); 82 | }); 83 | 84 | it("calculates gas cost for larger function call tx (bedrock)", function(){ 85 | const fn = optimismCases.bedrockFunction_2; 86 | options.gasPrice = fn.l2GasPrice; 87 | options.baseFee = fn.l1BaseFee; 88 | options.optimismHardfork = "bedrock"; 89 | 90 | const gas = getCalldataGasForNetwork(options, fn.tx); 91 | const cost = gasToCost(fn.l2GasUsed, gas, options); 92 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 93 | 94 | // Actual < 0.15% 95 | assert(diff < .01); 96 | }); 97 | 98 | it("calculates gas cost for deployment tx (bedrock)", function(){ 99 | const fn = optimismCases.bedrockDeployment; 100 | options.gasPrice = fn.l2GasPrice; 101 | options.baseFee = fn.l1BaseFee; 102 | options.optimismHardfork = "bedrock"; 103 | 104 | const gas = getCalldataGasForNetwork(options, fn.tx); 105 | const cost = gasToCost(fn.l2GasUsed, gas, options); 106 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 107 | 108 | // Actual < 0.06% 109 | assert(diff < .01); 110 | }); 111 | it("calculates gas cost for small function call tx (ecotone)", function () { 112 | const fn = optimismCases.ecotoneFunction_1; 113 | options.gasPrice = fn.l2GasPrice; 114 | options.baseFee = fn.l1BaseFee; 115 | options.blobBaseFee = fn.l1BlobBaseFee; 116 | options.optimismHardfork = "ecotone"; 117 | 118 | const gas = getCalldataGasForNetwork(options, fn.tx); 119 | const cost = gasToCost(fn.l2GasUsed, gas, options); 120 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 121 | 122 | // actual 0.013 123 | assert(diff < .015); 124 | }); 125 | 126 | it("calculates gas cost for large function call tx (ecotone) (I)", function () { 127 | const fn = optimismCases.ecotoneFunction_2; 128 | options.gasPrice = fn.l2GasPrice; 129 | options.baseFee = fn.l1BaseFee; 130 | options.blobBaseFee = fn.l1BlobBaseFee; 131 | options.optimismHardfork = "ecotone"; 132 | 133 | const gas = getCalldataGasForNetwork(options, fn.tx);; 134 | const cost = gasToCost(fn.l2GasUsed, gas, options); 135 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 136 | 137 | // actual 0.0105 138 | assert(diff < .015); 139 | }); 140 | 141 | it("calculates gas cost for large function call tx (ecotone) (II)", function () { 142 | const fn = optimismCases.ecotoneFunction_3; 143 | options.gasPrice = fn.l2GasPrice; 144 | options.baseFee = fn.l1BaseFee; 145 | options.blobBaseFee = fn.l1BlobBaseFee; 146 | options.optimismHardfork = "ecotone"; 147 | 148 | const gas = getCalldataGasForNetwork(options, fn.tx); 149 | const cost = gasToCost(fn.l2GasUsed, gas, options); 150 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 151 | 152 | // actual 0.0008 153 | assert(diff < .015); 154 | }); 155 | }); 156 | 157 | describe("Base: getCalldataCostForNetwork", function () { 158 | const options: GasReporterOptions = { 159 | L2: "base", 160 | tokenPrice: "1", 161 | currencyDisplayPrecision: 8, 162 | optimismHardfork: "ecotone", 163 | opStackBaseFeeScalar: BASE_ECOTONE_BASE_FEE_SCALAR, 164 | opStackBlobBaseFeeScalar: BASE_ECOTONE_BLOB_BASE_FEE_SCALAR 165 | } 166 | 167 | it("calculates gas cost for function call tx (ecotone)", function () { 168 | const fn = baseCases.ecotoneFunction_1; 169 | options.gasPrice = fn.l2GasPrice; 170 | options.baseFee = fn.l1BaseFee; 171 | options.blobBaseFee = fn.l1BlobBaseFee; 172 | 173 | const gas = getCalldataGasForNetwork(options, fn.tx); 174 | const cost = gasToCost(fn.l2GasUsed, gas, options); 175 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 176 | 177 | // Actual ~ 0.0005 178 | assert(diff < .01); 179 | }); 180 | }); 181 | 182 | describe("Arbitrum: getCalldataCostForNetwork", function () { 183 | const options: GasReporterOptions = { 184 | L2: "arbitrum", 185 | tokenPrice: "1", 186 | currencyDisplayPrecision: 8, 187 | } 188 | 189 | it("calculates gas cost for function call tx", function () { 190 | const fn = arbitrumCases.arbOSFunction_1; 191 | options.gasPrice = fn.l2GasPrice; 192 | options.baseFeePerByte = fn.l1BaseFeePerByte; 193 | 194 | const gas = getCalldataGasForNetwork(options, fn.tx); 195 | const cost = gasToCost(fn.l2GasUsed, gas, options); 196 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 197 | 198 | // Actual ~ 0.09 199 | assert(diff < .1); 200 | }); 201 | 202 | it("calculates gas cost for function call tx", function () { 203 | const fn = arbitrumCases.arbOSFunction_2; 204 | options.gasPrice = fn.l2GasPrice; 205 | options.baseFeePerByte = fn.l1BaseFeePerByte; 206 | 207 | const gas = getCalldataGasForNetwork(options, fn.tx); 208 | const cost = gasToCost(fn.l2GasUsed, gas, options); 209 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 210 | 211 | // Actual ~ 0.13 212 | assert(diff < .15); 213 | }); 214 | 215 | it("calculates gas cost for a deployment tx", function () { 216 | const fn = arbitrumCases.arbOSDeployment_1; 217 | options.gasPrice = fn.l2GasPrice; 218 | options.baseFeePerByte = fn.l1BaseFeePerByte; 219 | 220 | const gas = getCalldataGasForNetwork(options, fn.tx); 221 | const cost = gasToCost(fn.l2GasUsed, gas, options); 222 | const diff = getPercentDiff(parseFloat(cost), fn.txFeeETH); 223 | 224 | // Actual ~ 0.089 225 | assert(diff < .1); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /test/unit/prices.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { assert } from "chai"; 3 | import { 4 | setGasAndPriceRates 5 | } from "../../src/utils/prices"; 6 | import { getDefaultOptions } from "../../src/lib/options"; 7 | import { GasReporterOptions } from "../types"; 8 | 9 | describe("setGasAndPriceRates", function(){ 10 | let options: GasReporterOptions; 11 | 12 | beforeEach(() => { 13 | options = getDefaultOptions({}); 14 | }); 15 | 16 | it ("when tokenPrice and gasPrice are both set", async function(){ 17 | const initialTokenPrice = "1"; 18 | const initialGasPrice = 1; 19 | options.tokenPrice = initialTokenPrice; 20 | options.gasPrice = initialGasPrice; 21 | 22 | await setGasAndPriceRates(options); 23 | 24 | assert.equal(options.tokenPrice, initialTokenPrice); 25 | assert.equal(options.gasPrice, initialGasPrice); 26 | }) 27 | 28 | it ("when coinmarketcap is not set", async function() { 29 | assert.isUndefined(options.tokenPrice); 30 | assert.isUndefined(options.gasPrice); 31 | 32 | await setGasAndPriceRates(options); 33 | 34 | assert.isUndefined(options.tokenPrice); 35 | assert.isUndefined(options.gasPrice); 36 | }); 37 | 38 | it ("when offline is true", async function(){ 39 | assert.isUndefined(options.tokenPrice); 40 | assert.isUndefined(options.gasPrice); 41 | 42 | options.coinmarketcap = process.env.CMC_API_KEY; 43 | options.offline = true; 44 | 45 | await setGasAndPriceRates(options); 46 | 47 | assert.isUndefined(options.tokenPrice); 48 | assert.isUndefined(options.gasPrice); 49 | }); 50 | 51 | it ("when tokenPrice but not gasPrice is set", async function() { 52 | const initialTokenPrice = "1"; 53 | 54 | options.tokenPrice = initialTokenPrice; 55 | options.coinmarketcap = process.env.CMC_API_KEY; 56 | options.etherscan = process.env.ETHERSCAN_API_KEY; 57 | 58 | assert.isUndefined(options.gasPrice); 59 | 60 | await setGasAndPriceRates(options); 61 | 62 | assert.equal(options.tokenPrice, initialTokenPrice); 63 | assert.isDefined(options.gasPrice); 64 | assert.typeOf(options.gasPrice, "number"); 65 | }); 66 | 67 | it ("when gasPrice but not tokenPrice is set", async function(){ 68 | const initialGasPrice = 1; 69 | options.gasPrice = initialGasPrice; 70 | options.coinmarketcap = process.env.CMC_API_KEY; 71 | 72 | assert.isUndefined(options.tokenPrice); 73 | 74 | await setGasAndPriceRates(options); 75 | assert.equal(options.gasPrice, initialGasPrice); 76 | assert.isDefined(options.tokenPrice); 77 | assert.typeOf(options.tokenPrice, "string"); 78 | }); 79 | 80 | it("when tokenPrice and gasPrice are set but baseFeePerByte is not set (arbitrum)", async function(){ 81 | options.tokenPrice = "1"; 82 | options.gasPrice = 1; 83 | options.L2 = 'arbitrum'; 84 | options.coinmarketcap = process.env.CMC_API_KEY; 85 | options.etherscan = process.env.ETHERSCAN_API_KEY; 86 | 87 | assert.isUndefined(options.baseFeePerByte); 88 | 89 | await setGasAndPriceRates(options); 90 | 91 | assert.isDefined(options.baseFeePerByte); 92 | assert.typeOf(options.baseFeePerByte, "number"); 93 | }); 94 | 95 | it("when tokenPrice and gasPrice are set but baseFee is not set", async function(){ 96 | options.tokenPrice = "1"; 97 | options.gasPrice = 1; 98 | options.L2 = 'optimism'; 99 | options.coinmarketcap = process.env.CMC_API_KEY; 100 | options.etherscan = process.env.ETHERSCAN_API_KEY; 101 | 102 | assert.isUndefined(options.baseFee); 103 | 104 | await setGasAndPriceRates(options); 105 | 106 | assert.isDefined(options.baseFee); 107 | assert.typeOf(options.baseFee, "number"); 108 | }); 109 | 110 | it("when tokenPrice, gasPrice and baseFee are set but blobBaseFee is not set", async function(){ 111 | options.tokenPrice = "1"; 112 | options.gasPrice = 1; 113 | options.baseFee = 1; 114 | options.L2 = 'optimism'; 115 | options.optimismHardfork = "ecotone"; 116 | options.coinmarketcap = process.env.CMC_API_KEY; 117 | options.etherscan = process.env.ETHERSCAN_API_KEY; 118 | 119 | assert.isUndefined(options.blobBaseFee); 120 | 121 | await setGasAndPriceRates(options); 122 | 123 | assert.isDefined(options.blobBaseFee); 124 | assert.typeOf(options.blobBaseFee, "number"); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "strict": true, 10 | "rootDirs": ["./src", "./test"], 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true 13 | }, 14 | // "files": [], // Any hardhat plugin's main .d.ts has to be added here 15 | "exclude": ["dist", "node_modules"], 16 | "include": [ 17 | "./test", 18 | "./src" 19 | ] 20 | } -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "strict": false, 10 | "rootDirs": ["./src", "./test"], 11 | "esModuleInterop": true, 12 | "skipLibCheck": true 13 | }, 14 | // "files": [], // Any hardhat plugin's main .d.ts has to be added here 15 | "exclude": ["dist", "node_modules", "./test"], 16 | "include": [ 17 | "./src" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-plugin-prettier", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "prettier": true, 9 | "object-literal-sort-keys": false, 10 | "no-submodule-imports": false, 11 | "interface-name": false, 12 | "max-classes-per-file": false, 13 | "no-empty": false, 14 | "no-console": false, 15 | "only-arrow-functions": false, 16 | "variable-name": [ 17 | true, 18 | "check-format", 19 | "allow-leading-underscore", 20 | "allow-pascal-case" 21 | ], 22 | "ordered-imports": [ 23 | true, 24 | { 25 | "grouped-imports": true, 26 | "import-sources-order": "case-insensitive" 27 | } 28 | ], 29 | "no-floating-promises": true, 30 | "prefer-conditional-expression": false, 31 | "no-implicit-dependencies": true 32 | } 33 | } 34 | --------------------------------------------------------------------------------