├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── tasks ├── deploy │ ├── index.ts │ └── greeter.ts ├── storage-layout.ts └── accounts.ts ├── .czrc ├── .commitlintrc.yml ├── .solhintignore ├── .lintstagedrc.yml ├── .vscode ├── extensions.json └── settings.json ├── contract-downloader ├── requirements.txt ├── immunefi-get.py └── download.py ├── .solcover.js ├── .yarnrc.yml ├── .gitpod.yml ├── .editorconfig ├── .eslintignore ├── .prettierignore ├── .gitignore ├── test ├── greeter │ ├── Greeter.behavior.ts │ ├── Greeter.fixture.ts │ └── Greeter.ts ├── types.ts └── external │ ├── Attach.ts │ ├── AttachContract.ts │ └── helper.ts ├── .env.example ├── .prettierrc.yml ├── .solhint.json ├── .eslintrc.yml ├── tsconfig.json ├── .yarn ├── forkNetwork.js └── runTestWithContractAddress.js ├── contracts ├── Greeter.sol └── test │ └── Test.sol ├── .github ├── scripts │ └── rename.sh └── workflows │ ├── ci.yml.example │ └── create.yml ├── LICENSE.md ├── package.json ├── hardhat.config.ts └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /tasks/deploy/index.ts: -------------------------------------------------------------------------------- 1 | import "./greeter"; 2 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/artifacts 3 | **/node_modules 4 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.{js,json,md,sol,ts,yml}": 2 | - prettier --write 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn dlx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn dlx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "NomicFoundation.hardhat-solidity"] 3 | } 4 | -------------------------------------------------------------------------------- /contract-downloader/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv 2 | etherscan-python 3 | polygonscan-python 4 | bscscan-python 5 | beautifulsoup4 6 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | istanbulReporter: ["html", "lcov"], 3 | providerOptions: { 4 | mnemonic: process.env.MNEMONIC, 5 | }, 6 | skipFiles: ["test"], 7 | }; 8 | -------------------------------------------------------------------------------- /tasks/storage-layout.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | task("storage-layout", "Export the contracts' storage layouts", async (_taskArgs, hre) => { 4 | await hre.storageLayout.export(); 5 | }); 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: ".yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs" 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.2.1.cjs 8 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: "gitpod/workspace-node:latest" 2 | 3 | tasks: 4 | - init: "yarn install" 5 | 6 | vscode: 7 | extensions: 8 | - "esbenp.prettier-vscode" 9 | - "NomicFoundation.hardhat-solidity" 10 | - "ritwickdey.LiveServer" 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/ 3 | **/.coverage_artifacts 4 | **/.coverage_cache 5 | **/.coverage_contracts 6 | **/artifacts 7 | **/build 8 | **/cache 9 | **/coverage 10 | **/dist 11 | **/node_modules 12 | **/types 13 | 14 | # files 15 | *.env 16 | *.log 17 | .pnp.* 18 | coverage.json 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/ 3 | **/.coverage_artifacts 4 | **/.coverage_cache 5 | **/.coverage_contracts 6 | **/artifacts 7 | **/build 8 | **/cache 9 | **/coverage 10 | **/dist 11 | **/node_modules 12 | **/types 13 | 14 | # files 15 | *.env 16 | *.log 17 | .pnp.* 18 | coverage.json 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /tasks/accounts.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from "@ethersproject/abstract-signer"; 2 | import { task } from "hardhat/config"; 3 | 4 | task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => { 5 | const accounts: Signer[] = await hre.ethers.getSigners(); 6 | 7 | for (const account of accounts) { 8 | console.log(await account.getAddress()); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/releases 5 | !.yarn/plugins 6 | !.yarn/sdks 7 | !.yarn/versions 8 | **/artifacts 9 | **/build 10 | **/cache 11 | **/coverage 12 | **/.coverage_artifacts 13 | **/.coverage_cache 14 | **/.coverage_contracts 15 | **/dist 16 | **/node_modules 17 | **/__pycache__ 18 | src/types 19 | 20 | # files 21 | *.env 22 | *.log 23 | .pnp.* 24 | coverage.json 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[json]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[markdown]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[solidity]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[typescript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "editor.formatOnSave": true, 15 | "liveServer.settings.root": "/coverage" 16 | } 17 | -------------------------------------------------------------------------------- /test/greeter/Greeter.behavior.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | export function shouldBehaveLikeGreeter(): void { 4 | it("should return the new greeting once it's changed", async function () { 5 | expect(await this.greeter.connect(this.signers.admin).greet()).to.equal("Hello, world!"); 6 | 7 | await this.greeter.setGreeting("Bonjour, le monde!"); 8 | expect(await this.greeter.connect(this.signers.admin).greet()).to.equal("Bonjour, le monde!"); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | 3 | import type { Greeter } from "../src/types/contracts/Greeter"; 4 | 5 | type Fixture = () => Promise; 6 | 7 | declare module "mocha" { 8 | export interface Context { 9 | greeter: Greeter; 10 | loadFixture: (fixture: Fixture) => Promise; 11 | signers: Signers; 12 | } 13 | } 14 | 15 | export interface Signers { 16 | admin: SignerWithAddress; 17 | } 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INFURA_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 2 | MNEMONIC="here is where your twelve words mnemonic should be put my friend" 3 | 4 | # Block explorer API keys 5 | ARBISCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 6 | BSCSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 7 | ETHERSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 8 | OPTIMISM_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 9 | POLYGONSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 10 | SNOWTRACE_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 11 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | bracketSpacing: true 2 | printWidth: 120 3 | proseWrap: "always" 4 | singleQuote: false 5 | tabWidth: 2 6 | trailingComma: "all" 7 | endOfLine: "auto" 8 | 9 | overrides: 10 | - files: "*.sol" 11 | options: 12 | compiler: "0.8.17" 13 | tabWidth: 4 14 | - files: "*.ts" 15 | options: 16 | importOrder: ["", "^[./]"] 17 | importOrderParserPlugins: ["typescript"] 18 | importOrderSeparation: true 19 | importOrderSortSpecifiers: true 20 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 8], 6 | "compiler-version": ["error", ">=0.8.4"], 7 | "func-visibility": ["error", { "ignoreConstructors": true }], 8 | "max-line-length": ["error", 120], 9 | "not-rely-on-time": "off", 10 | "prettier/prettier": [ 11 | "error", 12 | { 13 | "endOfLine": "auto" 14 | } 15 | ], 16 | "reason-string": ["warn", { "maxLength": 64 }] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "eslint:recommended" 3 | - "plugin:@typescript-eslint/eslint-recommended" 4 | - "plugin:@typescript-eslint/recommended" 5 | - "prettier" 6 | parser: "@typescript-eslint/parser" 7 | parserOptions: 8 | project: "tsconfig.json" 9 | plugins: 10 | - "@typescript-eslint" 11 | root: true 12 | rules: 13 | "@typescript-eslint/no-floating-promises": 14 | - error 15 | - ignoreIIFE: true 16 | ignoreVoid: true 17 | "@typescript-eslint/no-inferrable-types": "off" 18 | "@typescript-eslint/no-unused-vars": 19 | - error 20 | - argsIgnorePattern: "_" 21 | varsIgnorePattern: "_" 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "emitDecoratorMetadata": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["es6"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noImplicitAny": true, 13 | "removeComments": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "es6" 18 | }, 19 | "exclude": ["node_modules"], 20 | "files": ["./hardhat.config.ts"], 21 | "include": ["src/**/*", "tasks/**/*", "test/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /.yarn/forkNetwork.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const { exit } = require("process"); 3 | 4 | // validate command line 5 | if (process.argv.length < 4 || process.argv[2] != "--network") { 6 | console.log("Usage: node forkNetwork --network "); 7 | console.log("Usage: yarn fork --network "); 8 | exit(1); 9 | } 10 | 11 | // get positional args 12 | const networkName = process.argv[3]; 13 | let otherArgs = ""; 14 | for (i = 4; i < process.argv.length; ++i) { 15 | otherArgs += " " + process.argv[i]; 16 | } 17 | 18 | try { 19 | execSync("npx cross-env NETWORK_NAME=" + networkName + " hardhat node" + otherArgs, { stdio: "inherit" }); 20 | } catch (ex) { 21 | // ignore when test fails, error output is given by 'hardhat test' anyways 22 | } 23 | -------------------------------------------------------------------------------- /contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.4; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "hardhat/console.sol"; 6 | 7 | error GreeterError(); 8 | 9 | contract Greeter is Ownable { 10 | string public greeting; 11 | 12 | constructor(string memory _greeting) { 13 | console.log("Deploying a Greeter with greeting:", _greeting); 14 | greeting = _greeting; 15 | } 16 | 17 | function greet() public view returns (string memory) { 18 | return greeting; 19 | } 20 | 21 | function setGreeting(string memory _greeting) public { 22 | console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); 23 | greeting = _greeting; 24 | } 25 | 26 | function throwError() external pure { 27 | revert GreeterError(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/greeter/Greeter.fixture.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | import { ethers } from "hardhat"; 3 | 4 | import type { Greeter } from "../../src/types/contracts/Greeter"; 5 | import type { Greeter__factory } from "../../src/types/factories/contracts/Greeter__factory"; 6 | 7 | export async function deployGreeterFixture(): Promise<{ greeter: Greeter }> { 8 | const signers: SignerWithAddress[] = await ethers.getSigners(); 9 | const admin: SignerWithAddress = signers[0]; 10 | 11 | const greeting: string = "Hello, world!"; 12 | const greeterFactory: Greeter__factory = await ethers.getContractFactory("Greeter"); 13 | const greeter: Greeter = await greeterFactory.connect(admin).deploy(greeting); 14 | await greeter.deployed(); 15 | 16 | return { greeter }; 17 | } 18 | -------------------------------------------------------------------------------- /tasks/deploy/greeter.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { task } from "hardhat/config"; 3 | import type { TaskArguments } from "hardhat/types"; 4 | 5 | import type { Greeter } from "../../src/types/contracts/Greeter"; 6 | import type { Greeter__factory } from "../../src/types/factories/contracts/Greeter__factory"; 7 | 8 | task("deploy:Greeter") 9 | .addParam("greeting", "Say hello, be nice") 10 | .setAction(async function (taskArguments: TaskArguments, { ethers }) { 11 | const signers: SignerWithAddress[] = await ethers.getSigners(); 12 | const greeterFactory: Greeter__factory = await ethers.getContractFactory("Greeter"); 13 | const greeter: Greeter = await greeterFactory.connect(signers[0]).deploy(taskArguments.greeting); 14 | await greeter.deployed(); 15 | console.log("Greeter deployed to: ", greeter.address); 16 | }); 17 | -------------------------------------------------------------------------------- /contracts/test/Test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.4; 3 | 4 | // 1. change to contract under test 5 | import "../Greeter.sol"; 6 | 7 | contract Test { 8 | address public immutable targetAddress; 9 | 10 | constructor(address _targetAddress) { 11 | targetAddress = _targetAddress; 12 | } 13 | 14 | function stringEqual(string memory str1, string memory str2) internal pure returns (bool) { 15 | return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2)); 16 | } 17 | 18 | // 2. implement test cases 19 | function mainTestCase() external { 20 | Greeter target = Greeter(targetAddress); 21 | 22 | require(stringEqual(target.greet(), "Hello, world!"), "Unexpected greeting"); 23 | 24 | target.setGreeting("Bonjour, le monde!"); 25 | require(stringEqual(target.greet(), "Bonjour, le monde!"), "Greeting was not set"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/greeter/Greeter.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 3 | import { ethers } from "hardhat"; 4 | 5 | import type { Signers } from "../types"; 6 | import { shouldBehaveLikeGreeter } from "./Greeter.behavior"; 7 | import { deployGreeterFixture } from "./Greeter.fixture"; 8 | 9 | describe("Unit tests", function () { 10 | before(async function () { 11 | this.signers = {} as Signers; 12 | 13 | const signers: SignerWithAddress[] = await ethers.getSigners(); 14 | this.signers.admin = signers[0]; 15 | 16 | this.loadFixture = loadFixture; 17 | }); 18 | 19 | describe("Greeter", function () { 20 | beforeEach(async function () { 21 | const { greeter } = await this.loadFixture(deployGreeterFixture); 22 | this.greeter = greeter; 23 | }); 24 | 25 | shouldBehaveLikeGreeter(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.github/scripts/rename.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca 4 | set -euo pipefail 5 | 6 | # Define the input vars 7 | GITHUB_REPOSITORY=${1?Error: Please pass username/repo, e.g. prb/hardhat-template} 8 | GITHUB_REPOSITORY_OWNER=${2?Error: Please pass username, e.g. prb} 9 | 10 | echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" 11 | echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER" 12 | 13 | # jq is like sed for JSON data 14 | JQ_OUTPUT=`jq \ 15 | --arg NAME "@$GITHUB_REPOSITORY" \ 16 | --arg AUTHOR_NAME "$GITHUB_REPOSITORY_OWNER" \ 17 | --arg URL "https://github.com/$GITHUB_REPOSITORY_OWNER" \ 18 | '.name = $NAME | .description = "" | .author |= ( .name = $AUTHOR_NAME | .url = $URL )' \ 19 | package.json 20 | ` 21 | 22 | # Overwrite package.json 23 | echo "$JQ_OUTPUT" > package.json 24 | 25 | # Rename instances of "paulrberg/hardhat-template" to the new repo name in README.md for badges only 26 | sed -i -e "/Use this template/! s|paulrberg/hardhat-template|"${GITHUB_REPOSITORY}"|;" "README.md" 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Paul Razvan Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml.example: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | env: 4 | DOTENV_CONFIG_PATH: "./.env.example" 5 | 6 | on: 7 | workflow_dispatch: 8 | pull_request: 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | ci: 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - name: "Check out the repo" 18 | uses: "actions/checkout@v3" 19 | 20 | - name: "Install Node.js" 21 | uses: "actions/setup-node@v3" 22 | with: 23 | cache: "yarn" 24 | node-version: "lts/*" 25 | 26 | - name: "Install the dependencies" 27 | run: "yarn install --immutable" 28 | 29 | - name: "Lint the code" 30 | run: "yarn lint" 31 | 32 | - name: "Add lint summary" 33 | run: | 34 | echo "## Lint results" >> $GITHUB_STEP_SUMMARY 35 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 36 | 37 | - name: "Compile the contracts and generate the TypeChain bindings" 38 | run: "yarn typechain" 39 | 40 | - name: "Test the contracts and generate the coverage report" 41 | run: "yarn coverage" 42 | 43 | - name: "Add test summary" 44 | run: | 45 | echo "## Test results" >> $GITHUB_STEP_SUMMARY 46 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 47 | -------------------------------------------------------------------------------- /.github/workflows/create.yml: -------------------------------------------------------------------------------- 1 | name: Create 2 | 3 | # The workflow will run only when `use this template` is used 4 | on: 5 | create: 6 | 7 | jobs: 8 | create: 9 | # We will only run this action when the repository isn't the template repository 10 | # Reference https://docs.github.com/en/actions/learn-github-actions/contexts 11 | # Reference https://docs.github.com/en/actions/learn-github-actions/expressions 12 | if: ${{ !github.event.repository.is_template }} 13 | 14 | runs-on: ubuntu-latest 15 | permissions: write-all 16 | 17 | steps: 18 | - name: "Check out the repo" 19 | uses: "actions/checkout@v3" 20 | 21 | - name: "Update package.json" 22 | run: ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" 23 | 24 | - name: "Add rename summary" 25 | run: | 26 | echo "## Commit results" >> $GITHUB_STEP_SUMMARY 27 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 28 | 29 | - name: "Update commit" 30 | uses: stefanzweifel/git-auto-commit-action@v4 31 | with: 32 | commit_message: "feat: initial commit" 33 | commit_options: "--amend" 34 | push_options: "--force" 35 | skip_fetch: true 36 | 37 | - name: "Add commit summary" 38 | run: | 39 | echo "## Commit results" >> $GITHUB_STEP_SUMMARY 40 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 41 | -------------------------------------------------------------------------------- /.yarn/runTestWithContractAddress.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const { exit } = require("process"); 3 | 4 | // validate command line 5 | if (process.argv.length < 3) { 6 | // from console? 7 | console.log("Usage: node runTestWithContractAddress [--impersonate
]"); 8 | exit(1); 9 | } 10 | if (process.argv.length < 4) { 11 | // from yarn? 12 | console.log("Usage: yarn attach "); 13 | exit(1); 14 | } 15 | 16 | let impersonateAddress = ""; 17 | let argIdx = 4; 18 | if (process.argv.length >= 6 && process.argv[4] == "--impersonate") { 19 | impersonateAddress = process.argv[5]; 20 | argIdx = 6; 21 | } 22 | 23 | // get positional args 24 | const testFile = process.argv[2]; 25 | const contractAddress = process.argv[3]; 26 | let otherArgs = ""; 27 | for (i = argIdx; i < process.argv.length; ++i) { 28 | otherArgs += " " + process.argv[i]; 29 | } 30 | 31 | try { 32 | if (impersonateAddress == "") { 33 | execSync( 34 | "npx cross-env CONTRACT_ADDRESS=" + 35 | contractAddress + 36 | " hardhat test " + 37 | testFile + 38 | otherArgs + 39 | " --network localhost", 40 | { stdio: "inherit" }, 41 | ); 42 | } else { 43 | execSync( 44 | "npx cross-env CONTRACT_ADDRESS=" + 45 | contractAddress + 46 | " IMPERSONATE_ADDRESS=" + 47 | impersonateAddress + 48 | " hardhat test " + 49 | testFile + 50 | otherArgs + 51 | " --network localhost", 52 | { stdio: "inherit" }, 53 | ); 54 | } 55 | } catch (ex) { 56 | // ignore when test fails, error output is given by 'hardhat test' anyways 57 | } 58 | -------------------------------------------------------------------------------- /test/external/Attach.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | import { expect } from "chai"; 3 | import { ethers } from "hardhat"; 4 | 5 | import * as contracts from "../../src/types"; 6 | import { addBalance_ETH, impersonate, printBalance_ERC20, printBalance_ETH, sendToken_ERC20 } from "./helper"; 7 | 8 | describe("Attach to external contract", async function () { 9 | let signer: SignerWithAddress; 10 | 11 | // 1. change to type of external contract 12 | let targetContract: contracts.Greeter; 13 | 14 | before(async function () { 15 | // address of external contract must be provided via env. variable 16 | const contractAddress: string = process.env.CONTRACT_ADDRESS; 17 | const impersonateAddress: string = process.env.IMPERSONATE_ADDRESS; 18 | 19 | if (impersonateAddress) { 20 | signer = await impersonate(impersonateAddress); 21 | } else { 22 | [signer] = await ethers.getSigners(); 23 | } 24 | 25 | /* 26 | // optional: prepare ETH and ERC20 balances 27 | const contractWETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; 28 | const holderWETH = "0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e"; 29 | await addBalance_ETH(signer.address, 1); 30 | await sendToken_ERC20(contractWETH, holderWETH, signer.address, 2.5); 31 | await printBalance_ETH(signer.address); 32 | await printBalance_ERC20(contractWETH, signer.address); 33 | */ 34 | 35 | // 2. change to name of external contract 36 | targetContract = await ethers.getContractAt("Greeter", contractAddress, signer); 37 | }); 38 | 39 | // 3. implement interactions with external contract 40 | it("Main test case", async function () { 41 | expect(await targetContract.connect(signer).greet()).to.equal("Hello, world!"); 42 | 43 | await targetContract.setGreeting("Bonjour, le monde!"); 44 | expect(await targetContract.connect(signer).greet()).to.equal("Bonjour, le monde!"); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/external/AttachContract.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | import { use as chaiUse, expect } from "chai"; 3 | import chaiAsPromised from "chai-as-promised"; 4 | import { ethers, network } from "hardhat"; 5 | 6 | import * as contracts from "../../src/types"; 7 | import { addBalance_ETH, printBalance_ERC20, printBalance_ETH, sendToken_ERC20 } from "./helper"; 8 | 9 | chaiUse(chaiAsPromised); 10 | 11 | describe("Attach test contract to external contract", async function () { 12 | let signer: SignerWithAddress; 13 | 14 | let test: contracts.Test; 15 | 16 | // optional: use external contract in test case 17 | //let targetContract: contracts.Greeter; 18 | 19 | before(async function () { 20 | // address of external contract must be provided via env. variable 21 | const contractAddress: string = process.env.CONTRACT_ADDRESS; 22 | const impersonateAddress: string = process.env.IMPERSONATE_ADDRESS; 23 | 24 | [signer] = await ethers.getSigners(); 25 | 26 | // deploy test contract and pass address of external contract 27 | const testFactory: contracts.Test__factory = await ethers.getContractFactory("Test"); 28 | test = await testFactory.connect(signer).deploy(contractAddress); 29 | await test.deployed(); 30 | 31 | if (impersonateAddress) { 32 | // get deployed code of Test contract and "deploy" it at 'impersonateAddress' 33 | const deployedCode = await ethers.provider.getCode(test.address); 34 | await network.provider.request({ method: "hardhat_setCode", params: [impersonateAddress, deployedCode] }); 35 | test = await ethers.getContractAt("Test", impersonateAddress, signer); 36 | } 37 | 38 | /* 39 | // optional: prepare ETH and ERC20 balances 40 | const contractWETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; 41 | const holderWETH = "0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e"; 42 | await addBalance_ETH(test.address, 1); 43 | await sendToken_ERC20(contractWETH, holderWETH, test.address, 2.5); 44 | await printBalance_ETH(test.address); 45 | await printBalance_ERC20(contractWETH, test.address); 46 | */ 47 | 48 | // optional: use external contract in test case 49 | //targetContract = await ethers.getContractAt("Greeter", contractAddress, signer); 50 | }); 51 | 52 | // optional: implement additional interactions with external and test contract 53 | it("Main test case", async function () { 54 | await expect(test.connect(signer).mainTestCase()).not.to.be.rejectedWith(Error); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /contract-downloader/immunefi-get.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import argparse 3 | from bs4 import BeautifulSoup 4 | 5 | import download 6 | 7 | # dict: block explorer name <-> network name (for contract downloader) 8 | supportedExplorers = { "etherscan": "mainnet", "polygonscan": "polygon", "bscscan": "bsc" } 9 | 10 | if __name__ == "__main__": 11 | try: 12 | # get contract address and network from command line 13 | parser = argparse.ArgumentParser(description='Gathers all block explorer links to verified smart contracts in scope from an Immunefi bug bounty page and forwards them to the downloader.') 14 | parser.add_argument('bountyUrl', type=str, help='bug bounty URL, e.g. https://immunefi.com/bounty/vulnerableproject/') 15 | parser.add_argument('-r', '--remove', action='store_true', help='remove bounty contracts from local filesystem') 16 | args = parser.parse_args() 17 | 18 | # download bug bounty page 19 | page = requests.get(args.bountyUrl) 20 | soup = BeautifulSoup(page.content, "html.parser") 21 | 22 | # find all assets in scope with hyperlink 23 | assetsInScope = soup.find("h3", string="Assets in scope").parent 24 | assetLinks = assetsInScope.find_all("a") 25 | contracts = [] 26 | 27 | # filter asset links to get contracts only 28 | for assetLink in assetLinks: 29 | assetLink = assetLink.text.strip() 30 | for explorer in supportedExplorers: 31 | if ("/"+explorer+".") in assetLink: # check if link to supported block explorer 32 | addressPos = assetLink.index('0x') 33 | contractAddress = assetLink[addressPos:addressPos+42] # get 20 bytes hex address from link 34 | contracts.append((explorer, contractAddress)) 35 | break 36 | 37 | # forward found contracts to downloader 38 | skippedContracts = [] 39 | compilerVersions = {} 40 | for contract in contracts: 41 | #print(contract) 42 | try: 43 | compilerVersions |= download.start(supportedExplorers[contract[0]], contract[1], args.remove, True) # always try to resolve implementation contract 44 | except Exception as e: 45 | print("Warning:", e) 46 | skippedContracts.append(contract) 47 | 48 | if len(skippedContracts) > 0: 49 | print("") 50 | print("Skipped contracts due to errors:") 51 | for contract in skippedContracts: 52 | print(contract) 53 | 54 | print("") 55 | print("Compiler versions:", [version for version in reversed(compilerVersions)]) 56 | print("Done!") 57 | except Exception as e: 58 | print("Error:", e) 59 | 60 | -------------------------------------------------------------------------------- /test/external/helper.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | import { ethers, network } from "hardhat"; 3 | 4 | const ERC20_InterfaceSubset = [ 5 | "function decimals() external view returns (uint8)", 6 | "function transfer(address to, uint256 amount) external returns (bool)", 7 | "function balanceOf(address account) external view returns (uint256)", 8 | "function symbol() external view returns (string memory)", 9 | ]; 10 | 11 | // get signer for given account address 12 | export async function impersonate(account: string): Promise { 13 | await network.provider.request({ method: "hardhat_impersonateAccount", params: [account] }); 14 | return ethers.getSigner(account); 15 | } 16 | 17 | // get account balance in ETH (not wei) 18 | export async function getBalance_ETH(account: string): Promise { 19 | const balance_wei = await ethers.provider.getBalance(account); 20 | return ethers.utils.formatEther(balance_wei); 21 | } 22 | export async function printBalance_ETH(account: string): Promise { 23 | console.log("Balance of", account, "|", "ETH", ":", await getBalance_ETH(account)); 24 | } 25 | 26 | // add ETH amount to account balance 27 | export async function addBalance_ETH(account: string, amount_eth: number): Promise { 28 | const balance_wei = await ethers.provider.getBalance(account); 29 | const amount_wei = ethers.utils.parseEther(amount_eth.toString()); 30 | const newHexBalance = balance_wei.add(amount_wei).toHexString().replace("0x0", "0x"); // without leading zeros 31 | await network.provider.request({ method: "hardhat_setBalance", params: [account, newHexBalance] }); 32 | } 33 | 34 | // get ERC20 account balance in token units (not base units) 35 | export async function getBalance_ERC20(token: string, account: string): Promise { 36 | const [signer] = await ethers.getSigners(); 37 | const tokenContract = new ethers.Contract(token, ERC20_InterfaceSubset, signer); 38 | const tokenDecimals = await tokenContract.decimals(); 39 | const balance_baseunits = await tokenContract.balanceOf(account); 40 | return ethers.utils.formatUnits(balance_baseunits, tokenDecimals); 41 | } 42 | export async function printBalance_ERC20(token: string, account: string): Promise { 43 | const [signer] = await ethers.getSigners(); 44 | const tokenContract = new ethers.Contract(token, ERC20_InterfaceSubset, signer); 45 | console.log("Balance of", account, "|", await tokenContract.symbol(), ":", await getBalance_ERC20(token, account)); 46 | } 47 | 48 | // send token amount in token units (not base units) from arbitrary account (will be impersonated) to given account 49 | export async function sendToken_ERC20(token: string, from: string, to: string, amount_units: number): Promise { 50 | const signer = await impersonate(from); // impersonate 'from' address 51 | await addBalance_ETH(from, 0.1); // fund 'from' address to pay for token transfer 52 | 53 | const tokenContract = new ethers.Contract(token, ERC20_InterfaceSubset, signer); 54 | 55 | const tokenDecimals = await tokenContract.decimals(); 56 | const amount_baseunits = ethers.utils.parseUnits(amount_units.toString(), tokenDecimals); 57 | 58 | await tokenContract.transfer(to, amount_baseunits); 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@MarioPoneder/solidity-audit-template", 3 | "description": "Solidity Coding, Testing and Audit Template", 4 | "version": "1.0.0", 5 | "contributors": [ 6 | { 7 | "name": "Paul Razvan Berg", 8 | "url": "https://github.com/paulrberg" 9 | }, 10 | { 11 | "name": "Mario Poneder", 12 | "url": "https://github.com/MarioPoneder" 13 | } 14 | ], 15 | "devDependencies": { 16 | "@commitlint/cli": "^17.3.0", 17 | "@commitlint/config-conventional": "^17.3.0", 18 | "@ethersproject/abi": "^5.7.0", 19 | "@ethersproject/abstract-signer": "^5.7.0", 20 | "@ethersproject/bignumber": "^5.7.0", 21 | "@ethersproject/bytes": "^5.7.0", 22 | "@ethersproject/providers": "^5.7.2", 23 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.4", 24 | "@nomicfoundation/hardhat-network-helpers": "^1.0.6", 25 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 26 | "@nomiclabs/hardhat-ethers": "^2.2.1", 27 | "@nomiclabs/hardhat-etherscan": "^3.1.2", 28 | "@openzeppelin/contracts": "^4.8.0", 29 | "@trivago/prettier-plugin-sort-imports": "^4.0.0", 30 | "@typechain/ethers-v5": "^10.1.1", 31 | "@typechain/hardhat": "^6.1.4", 32 | "@types/chai": "^4.3.4", 33 | "@types/chai-as-promised": "^7.1.5", 34 | "@types/fs-extra": "^9.0.13", 35 | "@types/mocha": "^10.0.0", 36 | "@types/node": "^18.11.9", 37 | "@typescript-eslint/eslint-plugin": "^5.44.0", 38 | "@typescript-eslint/parser": "^5.44.0", 39 | "chai": "^4.3.7", 40 | "chai-as-promised": "^7.1.1", 41 | "commitizen": "^4.2.5", 42 | "cross-env": "^7.0.3", 43 | "cz-conventional-changelog": "^3.3.0", 44 | "dotenv": "^16.0.3", 45 | "eslint": "^8.28.0", 46 | "eslint-config-prettier": "^8.5.0", 47 | "ethers": "^5.7.2", 48 | "fs-extra": "^10.1.0", 49 | "hardhat": "^2.12.7", 50 | "hardhat-gas-reporter": "^1.0.9", 51 | "hardhat-storage-layout": "^0.1.7", 52 | "hardhat-tracer": "^1.2.0", 53 | "husky": "^8.0.2", 54 | "lint-staged": "^13.0.4", 55 | "lodash": "^4.17.21", 56 | "mocha": "^10.1.0", 57 | "pinst": "^3.0.0", 58 | "prettier": "^2.8.0", 59 | "prettier-plugin-solidity": "^1.0.0", 60 | "shx": "^0.3.4", 61 | "solhint": "^3.3.7", 62 | "solhint-plugin-prettier": "^0.0.5", 63 | "solidity-coverage": "^0.8.2", 64 | "ts-generator": "^0.1.1", 65 | "ts-node": "^10.9.1", 66 | "typechain": "^8.1.1", 67 | "typescript": "^4.9.3" 68 | }, 69 | "files": [ 70 | "/contracts" 71 | ], 72 | "keywords": [ 73 | "blockchain", 74 | "ethers", 75 | "ethereum", 76 | "hardhat", 77 | "smart-contracts", 78 | "solidity", 79 | "template", 80 | "typescript", 81 | "typechain" 82 | ], 83 | "packageManager": "yarn@3.2.1", 84 | "publishConfig": { 85 | "access": "public" 86 | }, 87 | "scripts": { 88 | "clean": "shx rm -rf ./artifacts ./cache ./coverage ./src/types ./coverage.json ./slither_report.md", 89 | "cleanContracts": "git clean contracts -d -f", 90 | "cleanAll": "yarn clean && yarn cleanContracts", 91 | "commit": "git-cz", 92 | "compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile", 93 | "coverage": "yarn typechain && hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"test/[!external]*/*.ts\" && yarn typechain", 94 | "deploy": "hardhat deploy:Greeter --network localhost", 95 | "lint": "yarn lint:sol && yarn lint:ts && yarn prettier:check", 96 | "lint:sol": "solhint --max-warnings 0 \"contracts/**/*.sol\"", 97 | "lint:ts": "eslint --ignore-path ./.eslintignore --ext .js,.ts .", 98 | "postinstall": "husky install && DOTENV_CONFIG_PATH=./.env.example yarn typechain", 99 | "postpublish": "pinst --enable", 100 | "prepublishOnly": "pinst --disable", 101 | "prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"", 102 | "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", 103 | "test": "hardhat test test/greeter/Greeter.ts", 104 | "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain", 105 | "storage": "hardhat storage-layout", 106 | "fork": "hardhat node", 107 | "forkNetwork": "node .yarn/forkNetwork", 108 | "clone": "python contract-downloader/download.py", 109 | "immunefi": "python contract-downloader/immunefi-get.py", 110 | "attach": "node .yarn/runTestWithContractAddress test/external/Attach", 111 | "attachContract": "node .yarn/runTestWithContractAddress test/external/AttachContract", 112 | "slither": "slither . --checklist > slither_report.md" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@nomicfoundation/hardhat-toolbox"; 2 | import { config as dotenvConfig } from "dotenv"; 3 | import "hardhat-storage-layout"; 4 | import "hardhat-tracer"; 5 | import { HardhatUserConfig } from "hardhat/config"; 6 | import { HardhatNetworkForkingUserConfig, HardhatNetworkUserConfig, SolcUserConfig } from "hardhat/types"; 7 | import { resolve } from "path"; 8 | 9 | import "./tasks/accounts"; 10 | import "./tasks/deploy"; 11 | import "./tasks/storage-layout"; 12 | 13 | const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || "./.env"; 14 | dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) }); 15 | 16 | // Ensure that we have all the environment variables we need. 17 | const mnemonic: string | undefined = process.env.MNEMONIC; 18 | if (!mnemonic) { 19 | throw new Error("Please set your MNEMONIC in a .env file"); 20 | } 21 | 22 | const infuraApiKey: string | undefined = process.env.INFURA_API_KEY; 23 | if (!infuraApiKey) { 24 | throw new Error("Please set your INFURA_API_KEY in a .env file"); 25 | } 26 | 27 | const chainIds: { [name: string]: number } = { 28 | "arbitrum-mainnet": 42161, 29 | avalanche: 43114, 30 | bsc: 56, 31 | hardhat: 31337, 32 | mainnet: 1, 33 | "optimism-mainnet": 10, 34 | "polygon-mainnet": 137, 35 | "polygon-mumbai": 80001, 36 | sepolia: 11155111, 37 | moonbeam: 1284, 38 | "aurora-mainnet": 1313161554, 39 | }; 40 | 41 | function getChainConfig(chain: string): HardhatNetworkUserConfig & HardhatNetworkForkingUserConfig { 42 | let jsonRpcUrl: string; 43 | switch (chain) { 44 | case "avalanche": 45 | jsonRpcUrl = "https://api.avax.network/ext/bc/C/rpc"; 46 | break; 47 | case "bsc": 48 | jsonRpcUrl = "https://rpc.ankr.com/bsc"; 49 | break; 50 | case "moonbeam": 51 | jsonRpcUrl = "https://moonbeam.api.onfinality.io/public"; 52 | break; 53 | default: 54 | jsonRpcUrl = "https://" + chain + ".infura.io/v3/" + infuraApiKey; 55 | } 56 | return { 57 | accounts: { 58 | count: 10, 59 | mnemonic, 60 | path: "m/44'/60'/0'/0", 61 | }, 62 | chainId: chainIds[chain], 63 | url: jsonRpcUrl, 64 | }; 65 | } 66 | 67 | function getCompilerSettings(versions: string[]): SolcUserConfig[] { 68 | const compilerSettings: SolcUserConfig[] = []; 69 | 70 | for (const ver of versions) { 71 | const solSettings: any = { 72 | metadata: { 73 | // Not including the metadata hash 74 | // https://github.com/paulrberg/solidity-template/issues/31 75 | bytecodeHash: "none", 76 | }, 77 | // Disable the optimizer when debugging 78 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 79 | optimizer: { 80 | enabled: true, 81 | runs: 800, 82 | }, 83 | }; 84 | 85 | // metadata key is not supported in solc versions < 0.6.0 86 | if (ver.localeCompare("0.6.0", undefined, { numeric: true, sensitivity: "base" }) < 0) { 87 | delete solSettings.metadata; 88 | } 89 | 90 | // add to list of settings 91 | compilerSettings.push({ 92 | version: ver, 93 | settings: solSettings, 94 | }); 95 | } 96 | 97 | return compilerSettings; 98 | } 99 | 100 | const config: HardhatUserConfig = { 101 | defaultNetwork: "hardhat", 102 | etherscan: { 103 | apiKey: { 104 | arbitrumOne: process.env.ARBISCAN_API_KEY || "", 105 | avalanche: process.env.SNOWTRACE_API_KEY || "", 106 | bsc: process.env.BSCSCAN_API_KEY || "", 107 | mainnet: process.env.ETHERSCAN_API_KEY || "", 108 | optimisticEthereum: process.env.OPTIMISM_API_KEY || "", 109 | polygon: process.env.POLYGONSCAN_API_KEY || "", 110 | polygonMumbai: process.env.POLYGONSCAN_API_KEY || "", 111 | sepolia: process.env.ETHERSCAN_API_KEY || "", 112 | }, 113 | }, 114 | gasReporter: { 115 | currency: "USD", 116 | enabled: process.env.REPORT_GAS ? true : false, 117 | excludeContracts: [], 118 | src: "./contracts", 119 | }, 120 | networks: { 121 | hardhat: { 122 | accounts: { 123 | mnemonic, 124 | }, 125 | forking: getChainConfig(process.env.NETWORK_NAME || "mainnet"), 126 | }, 127 | arbitrum: getChainConfig("arbitrum-mainnet"), 128 | avalanche: getChainConfig("avalanche"), 129 | bsc: getChainConfig("bsc"), 130 | mainnet: getChainConfig("mainnet"), 131 | optimism: getChainConfig("optimism-mainnet"), 132 | "polygon-mainnet": getChainConfig("polygon-mainnet"), 133 | "polygon-mumbai": getChainConfig("polygon-mumbai"), 134 | sepolia: getChainConfig("sepolia"), 135 | moonbeam: getChainConfig("moonbeam"), 136 | aurora: getChainConfig("aurora-mainnet"), 137 | }, 138 | paths: { 139 | artifacts: "./artifacts", 140 | cache: "./cache", 141 | sources: "./contracts", 142 | tests: "./test", 143 | }, 144 | solidity: { 145 | compilers: getCompilerSettings(["0.8.17"]), 146 | }, 147 | typechain: { 148 | outDir: "src/types", 149 | target: "ethers-v5", 150 | }, 151 | }; 152 | 153 | export default config; 154 | -------------------------------------------------------------------------------- /contract-downloader/download.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import argparse 4 | from dotenv import load_dotenv 5 | 6 | from etherscan import Etherscan 7 | from polygonscan import PolygonScan 8 | from bscscan import BscScan 9 | 10 | 11 | def _fakeInstallModule(sourceFilePath): 12 | if "node_modules" in sourceFilePath: 13 | modulePath = os.path.dirname(sourceFilePath) 14 | while not os.path.dirname(os.path.dirname(modulePath)).endswith("node_modules"): 15 | modulePath = os.path.dirname(modulePath) 16 | 17 | modulePackageFilePath = modulePath + "/package.json" 18 | if not os.path.exists(modulePackageFilePath): 19 | with open(modulePackageFilePath, 'w', encoding='utf-8') as f: 20 | f.write("{ \"name\": \"\", \"version\": \"\" }") 21 | 22 | 23 | def _download(eth, contractAddress, remove, resolveImpl): 24 | # get contract source code + dependencies 25 | contracts = eth.get_contract_source_code(contractAddress) 26 | versions = {} 27 | 28 | for contract in contracts: 29 | contractName = contract["ContractName"] 30 | if contractName == "": 31 | raise Exception("No code found at this address!") 32 | 33 | compilerVersion = contract["CompilerVersion"] 34 | print("----- Contract:", contractName, "| Compiler:", compilerVersion, "-----") 35 | if resolveImpl and contract["Proxy"] == "1": 36 | implAddress = contract["Implementation"] 37 | print("Proxy! -> Using implementation contract", implAddress, "instead ...") 38 | versions |= _download(eth, implAddress, remove, False) 39 | continue 40 | 41 | versions[compilerVersion[1:compilerVersion.index("+")]] = True # e.g. parse '0.7.6' from 'v0.7.6+commit.7338295f' and add to dict 42 | 43 | # parse contract source code + dependencies form JSON 44 | try: 45 | sourceFiles = json.loads(contract["SourceCode"][1:-1])["sources"] 46 | except json.decoder.JSONDecodeError: 47 | print("Warning: Failed to get individual source files. Contract was probably merged to single file upon verification.") 48 | sourceFiles = { contractName + ".sol" : { "content" : contract["SourceCode"] } } 49 | 50 | # replicate directory tree of contract source code + dependencies 51 | for sourceFileReference in sourceFiles: 52 | sourceFilePath = sourceFileReference 53 | isModule = sourceFilePath[0] == "@" 54 | 55 | if isModule: # put modules in 'node_modules' 56 | sourceFilePath = "node_modules/" + sourceFilePath 57 | elif not (sourceFilePath.startswith("contracts/") or sourceFilePath.startswith("/contracts/")): 58 | sourceFilePath = "contracts/" + sourceFilePath 59 | 60 | # make absolute path 61 | sourceFilePath = os.path.abspath("./" + sourceFilePath) 62 | print(os.path.relpath(sourceFilePath)) 63 | 64 | if not remove: 65 | os.makedirs(os.path.dirname(sourceFilePath), exist_ok=True) 66 | if isModule: 67 | _fakeInstallModule(sourceFilePath) 68 | 69 | with open(sourceFilePath, 'w', encoding='utf-8') as f: 70 | f.write(sourceFiles[sourceFileReference]["content"]) 71 | else: 72 | try: 73 | os.remove(sourceFilePath) 74 | parentDir = os.path.dirname(sourceFilePath) 75 | for i in range(4): # try to remove empty parent dirs 76 | os.rmdir(parentDir) 77 | parentDir = os.path.dirname(parentDir) 78 | except Exception: 79 | pass 80 | 81 | return versions 82 | 83 | 84 | def start(network, contractAddress, remove, resolveImpl): 85 | # get API keys from .env file 86 | load_dotenv() 87 | etherscanApiKey = os.getenv('ETHERSCAN_API_KEY') 88 | polygonscanApiKey = os.getenv('POLYGONSCAN_API_KEY') 89 | bscscanApiKey = os.getenv('BSCSCAN_API_KEY') 90 | 91 | if not remove: 92 | print("Downloading", network, "contract", contractAddress, "...") 93 | else: 94 | print("Removing", network, "contract", contractAddress, "...") 95 | 96 | compilerVersions = {} 97 | if network == "mainnet": 98 | eth = Etherscan(etherscanApiKey) 99 | compilerVersions = _download(eth, contractAddress, remove, resolveImpl) 100 | elif network == "polygon": 101 | with PolygonScan(polygonscanApiKey, False) as eth: 102 | compilerVersions = _download(eth, contractAddress, remove, resolveImpl) 103 | elif network == "bsc": 104 | with BscScan(bscscanApiKey, False) as eth: 105 | compilerVersions = _download(eth, contractAddress, remove, resolveImpl) 106 | else: 107 | print("Unsupported network!") 108 | 109 | return compilerVersions 110 | 111 | 112 | if __name__ == "__main__": 113 | try: 114 | # get contract address and network from command line 115 | parser = argparse.ArgumentParser(description='Downloads a verified smart contract and its dependencies from Etherscan, etc.') 116 | parser.add_argument('contractAddress', type=str, help='address of a verified contract') 117 | parser.add_argument('-n', '--network', type=str, help='network: mainnet, polygon or bsc (default=mainnet)', default='mainnet') 118 | parser.add_argument('-i', '--impl', action='store_true', help='if specified contract is proxy: resolve and download implementation instead') 119 | parser.add_argument('-r', '--remove', action='store_true', help='remove previously downloaded contract from local filesystem') 120 | args = parser.parse_args() 121 | 122 | start(args.network, args.contractAddress, args.remove, args.impl) 123 | 124 | print("") 125 | print("Done!") 126 | except Exception as e: 127 | print("Error:", e) 128 | 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidity Coding, Testing and Audit Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Hardhat][hardhat-badge]][hardhat] [![License: MIT][license-badge]][license] 2 | 3 | [gitpod]: https://gitpod.io/#https://github.com/MarioPoneder/solidity-audit-template 4 | [gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod 5 | [hardhat]: https://hardhat.org/ 6 | [hardhat-badge]: https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg 7 | [license]: https://opensource.org/licenses/MIT 8 | [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg 9 | 10 | My favorite setup for writing Solidity smart contracts as well as auditing/testing external contracts. 11 | 12 | - [Hardhat](https://github.com/nomiclabs/hardhat): compile, run and test smart contracts on a local development network 13 | - [TypeChain](https://github.com/ethereum-ts/TypeChain): generate TypeScript bindings for smart contracts 14 | - [Ethers](https://github.com/ethers-io/ethers.js/): renowned Ethereum library and wallet implementation 15 | - [Solhint](https://github.com/protofire/solhint): code linter 16 | - [Solcover](https://github.com/sc-forks/solidity-coverage): code coverage 17 | - [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity): code formatter 18 | - [Tracer](https://github.com/zemse/hardhat-tracer): trace events, calls and storage operations 19 | - [Storage Layout](https://github.com/aurora-is-near/hardhat-storage-layout): generate smart contract storage layout 20 | - Fork the mainnet or another EVM based network as a Hardhat Network instance 21 | - Download external contracts and their dependencies (via Python script) 22 | - Gather contracts in scope from Immuenfi bug bounty (via Python script) 23 | - Attach tests to external contracts and impersonate accounts (in mainnet fork) 24 | 25 | ## Getting Started 26 | 27 | Click the [`Use this template`](https://github.com/MarioPoneder/solidity-audit-template/generate) button at the top of 28 | the page to create a new repository with this repo as the initial state. 29 | 30 | ## Features 31 | 32 | This template builds upon the frameworks and libraries mentioned above, so for details about their specific features, 33 | please consult their respective documentations. 34 | 35 | For example, for Hardhat, you can refer to the [Hardhat Tutorial](https://hardhat.org/tutorial) and the 36 | [Hardhat Docs](https://hardhat.org/docs). You might be in particular interested in reading the 37 | [Testing Contracts](https://hardhat.org/tutorial/testing-contracts) section. 38 | 39 | ### Sensible Defaults 40 | 41 | This template comes with sensible default configurations in the following files: 42 | 43 | ```text 44 | ├── .commitlintrc.yml 45 | ├── .editorconfig 46 | ├── .eslintignore 47 | ├── .eslintrc.yml 48 | ├── .gitignore 49 | ├── .prettierignore 50 | ├── .prettierrc.yml 51 | ├── .solcover.js 52 | ├── .solhintignore 53 | ├── .solhint.json 54 | ├── .yarnrc.yml 55 | └── hardhat.config.ts 56 | ``` 57 | 58 | ### GitHub Actions 59 | 60 | This template comes with GitHub Actions pre-configured (disabled per default). Your contracts will be linted and tested 61 | on every push and pull request made to the `main` branch. 62 | 63 | Note though that to make this work, you must use your `INFURA_API_KEY` and your `MNEMONIC` as GitHub secrets and rename 64 | the CI script to `ci.yml` in order to enable it. 65 | 66 | You can edit the CI script in [.github/workflows/ci.yml.example](./.github/workflows/ci.yml.example). 67 | 68 | ### Conventional Commits 69 | 70 | This template enforces the [Conventional Commits](https://www.conventionalcommits.org/) standard for git commit 71 | messages. This is a lightweight convention that creates an explicit commit history, which makes it easier to write 72 | automated tools on top of. 73 | 74 | ### Git Hooks 75 | 76 | This template uses [Husky](https://github.com/typicode/husky) to run automated checks on commit messages, and 77 | [Lint Staged](https://github.com/okonet/lint-staged) to automatically format the code with Prettier when making a git 78 | commit. 79 | 80 | ## Usage 81 | 82 | ### Pre Requisites 83 | 84 | Before being able to run any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an 85 | environment variable. You can follow the example in `.env.example`. If you don't already have a mnemonic, you can use 86 | this [website](https://iancoleman.io/bip39/) to generate one. 87 | 88 | Then, proceed with installing dependencies: 89 | 90 | ```sh 91 | $ yarn install 92 | $ pip install -r contract-downloader/requirements.txt # for Python contract downloader 93 | ``` 94 | 95 | ### Example usage: External contract testing 96 | 97 | 1. Download external contract + dependencies or download contracts from Immunefi bug bounty 98 | 99 | ```sh 100 | $ yarn clone 101 | # OR 102 | $ yarn immunefi 103 | ``` 104 | 105 | 2. Set Solidity version in `hardhat.config.ts` 106 | 3. Compile contract(s) and generate typings 107 | 108 | ```sh 109 | $ yarn compile 110 | ``` 111 | 112 | 4. Export the contracts' storage layouts 113 | 114 | ```sh 115 | $ yarn storage 116 | ``` 117 | 118 | 5. Fork the mainnet as a local Hardhat Network instance 119 | 120 | ```sh 121 | $ yarn fork 122 | ``` 123 | 124 | 6. Adapt the test templates to break/exploit the external contract in the local Hardhat Network instance 125 | 126 | ```sh 127 | $ yarn attach 128 | $ yarn attachContract 129 | ``` 130 | 131 | ### Compile 132 | 133 | Compile the smart contracts with Hardhat: 134 | 135 | ```sh 136 | $ yarn compile 137 | ``` 138 | 139 | ### TypeChain 140 | 141 | Compile the smart contracts and generate TypeChain bindings: 142 | 143 | ```sh 144 | $ yarn typechain 145 | ``` 146 | 147 | ### Test 148 | 149 | Run the Mocha test for the example Greeter contract: 150 | 151 | ```sh 152 | $ yarn test 153 | ``` 154 | 155 | ### Lint Solidity 156 | 157 | Lint the Solidity code: 158 | 159 | ```sh 160 | $ yarn lint:sol 161 | ``` 162 | 163 | ### Lint TypeScript 164 | 165 | Lint the TypeScript code: 166 | 167 | ```sh 168 | $ yarn lint:ts 169 | ``` 170 | 171 | ### Coverage 172 | 173 | Generate the code coverage report: 174 | 175 | ```sh 176 | $ yarn coverage 177 | ``` 178 | 179 | ### Report Gas 180 | 181 | See the gas usage per unit test and average gas per method call: 182 | 183 | ```sh 184 | $ REPORT_GAS=true 185 | $ yarn test 186 | ``` 187 | 188 | ### Tracer 189 | 190 | Shows events, calls and storage operations when running the tests: 191 | 192 | ```sh 193 | $ yarn test --trace # shows logs + calls 194 | $ yarn test --fulltrace # shows logs + calls + sloads + sstores 195 | ``` 196 | 197 | ### Storage Layout 198 | 199 | Shows the compiled contracts' storage layouts: 200 | 201 | ```sh 202 | $ yarn storage 203 | ``` 204 | 205 | ### Mainnet Fork 206 | 207 | Starts an instance of Hardhat Network that forks mainnet. This means that it will simulate having the same state as 208 | mainnet, but it will work as a local development network. That way you can interact with deployed protocols and test 209 | complex interactions locally. 210 | 211 | To use this feature you need to set your Infura API key in the `.env` file. 212 | 213 | ```sh 214 | $ yarn fork 215 | $ yarn fork --fork-block-number # pin the block number 216 | ``` 217 | 218 | ### Network Fork 219 | 220 | Starts an instance of Hardhat Network that forks an EVM based network. Supported networks are given by `chainIds[]` in 221 | `hardhat.config.ts`. 222 | 223 | ```sh 224 | $ yarn forkNetwork --network # e.g. rinkeby or polygon-mainnet 225 | ``` 226 | 227 | ### Clone (with Python contract downloader) 228 | 229 | Downloads a verified smart contract and its dependencies from Etherscan, etc. To use this feature you need to set the 230 | relevant API keys in the `.env` file. 231 | 232 | ```sh 233 | $ yarn clone 234 | $ yarn clone --network # e.g. polygon or bsc 235 | ``` 236 | 237 | In order to remove a previously downloaded smart contract and its dependencies from the local filesystem, run: 238 | 239 | ```sh 240 | $ yarn clone --remove 241 | ``` 242 | 243 | Furthermore, implementation contracts can be downloaded through proxies by: 244 | 245 | ```sh 246 | $ yarn clone --impl 247 | ``` 248 | 249 | ### Immunefi (with Python contract downloader) 250 | 251 | Gathers all block explorer links to verified smart contracts in scope from an Immunefi bug bounty page and forwards them 252 | to the downloader, see [Clone](#clone). 253 | 254 | ```sh 255 | $ yarn immunefi 256 | $ yarn immunefi --remove # delete contracts 257 | ``` 258 | 259 | ### Attach test to external contract 260 | 261 | Attaches the Mocha test `external/Attach` to a deployed contract in your local Hardhat Network (e.g. mainnet fork). The 262 | test contains sample code for the Greeter contract and therefore needs to be adapted according to your needs. 263 | 264 | ```sh 265 | $ yarn attach [--impersonate
] 266 | ``` 267 | 268 | Features like [Report Gas](#report-gas) and [Tracer](#tracer) can also be used with this test. 269 | 270 | ### Attach test contract to external contract 271 | 272 | Attaches the Mocha test `external/AttachContract` and the contract `test/Test` to a deployed contract in your local 273 | Hardhat Network (e.g. mainnet fork). The test contains sample code for the Greeter contract and therefore needs to be 274 | adapted according to your needs. 275 | 276 | ```sh 277 | $ yarn attachContract [--impersonate
] 278 | ``` 279 | 280 | Features like [Report Gas](#report-gas) and [Tracer](#tracer) can also be used with this test. 281 | 282 | ### Clean 283 | 284 | Delete the smart contract artifacts, the coverage reports and the Hardhat cache: 285 | 286 | ```sh 287 | $ yarn clean 288 | ``` 289 | 290 | ### Clean contracts 291 | 292 | Delete all non-template contracts from the contract directory: 293 | 294 | ```sh 295 | $ yarn cleanContracts 296 | ``` 297 | 298 | ### Clean all 299 | 300 | Combines [Clean](#clean) and [Clean contracts](#clean-contracts): 301 | 302 | ```sh 303 | $ yarn cleanAll 304 | ``` 305 | 306 | ### Deploy 307 | 308 | Deploy the example Greeter contract to the Hardhat Network: 309 | 310 | ```sh 311 | $ yarn deploy --greeting "Hello, world!" 312 | ``` 313 | 314 | ## Tips 315 | 316 | ### Syntax Highlighting 317 | 318 | If you use VSCode, you can get Solidity syntax highlighting with the 319 | [hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension. 320 | 321 | ## Using GitPod 322 | 323 | [GitPod](https://www.gitpod.io/) is an open-source developer platform for remote development. 324 | 325 | To view the coverage report generated by `yarn coverage`, just click `Go Live` from the status bar to turn the server 326 | on/off. 327 | 328 | ## License 329 | 330 | [MIT](./LICENSE.md) © Paul Razvan Berg & Mario Poneder 331 | --------------------------------------------------------------------------------